diff --git a/eip-0038.md b/eip-0038.md new file mode 100644 index 00000000..9ac6763b --- /dev/null +++ b/eip-0038.md @@ -0,0 +1,394 @@ +Partial Voting Hard Fork +=============================== + +* Author: WilfordGrimley +* Status: Proposed +* Created: 06-Oct-2022 +* Last edited: 18-Oct-2022 +* License: CC0 +* Forking: Hard Fork Needed + + +Motivation +---------- + +The goal of this proposal is to return governance power to miners to give it parity with Ergo’s launch. When Ergo launched, it’s mining algorithm Autolykos +did not support outsourcability, and as such each miner was forced to solo mine. Ergo’s governance model, requiring a 90% supermajority to pass soft and hard forks, +was built with the same assumptions: that each miner would be solo mining and thus would vote for or against changes to the network as they see fit. +With Autolykos 2, non-outsourcability was disabled such that miners could join traditional stratum pools. This changed the assumptions of Ergo’s governance model: +a solo miner now has to compete with pooled miners for blocks (and therefor their right to vote). A miner could choose to join a stratum pool, but are dependent on +the pool developers and their willingness and capability to support a miner’s vote. Furthermore, even if a pool holds an internal vote in some way, the pool in still +limited by the protocol to be either entirely for or against any given change: regardless of a pool’s desire to abide it’s miners' votes accurately, it still must +censor some of them (unless they are unanimous). + +By enabling partial voting, mining pools that desire to give governance rights back to miners need only track their miner’s votes, and could submit them directly to +the network. (e.g. 91.56% of the hashrate on GetBlok is in favor of EIP 39 when it mines a block. GetBlok’s submits a block header with that block with the value +of 0.9156 in favour of the fork; WoolyPool mines the next block, 82% in favour, submitting a 0.82 value in favour of the fork; GetBlok mines the next block, this +time their miners indicated 0.931 in favour of the change, etc, etc.) + +There is an alternative to hard-forking to achieve similar results but it has large hardware overhead requirement for central pool operators, and requires splitting +the hashrate of traditonal mining pools into smaller fragments. The viability of this no-fork solution should be explored more thoroughly before forcing order on this hard-fork solution, as future decentralized smartpool development could render the need for this EIP obsolete. +The greatest downside to this solution is that the nature of voting occuring over epochs means that miners that choose to support smaller pools, or solo mine using their own nodes, will decrease the odds that blocks mined with their voting preference (when compared with a protcol that can support partial voting). While this solution should be pursured regardless for its benefits to network security, there will always be incentive to join larger traditional pools supporting a miner's preference during voting epochs. + +In summary: Disabling non-outsourcability introduced the ability and necessity for pools to censor a nonunanimous group of miners, and an incentive for miners to join pools during voting epochs to have their votes counted. By enabling additional validation logic to count partial votes, we can enable pools to report their miner's votes accurately, mitigate censorship, and return governance power to miners. + + +The Hard Fork Solution +------------------------- + +We introduce new validation logic to the Ergo Node such that miners may report partial support for any given change, while maintaining all other rules. As old nodes would be incapable of recogizing this partial support, a hard fork is required as attempting to soft-fork would likely result in future chainsplitting. + +This change will enable pools of miners to indicate their support for given changes provided the pool operator has a method for tracking miner preference. + +This is achieved by changing the boolean nature of votes to allow a range of potential support, and expanding the validation logic to include a collection of data representing the node's support percentage on each proposed change. + +A new parameter, maxVotingAccuracy is defined and can be modified by future voting that determines the accuracy with with votes are counted. + +Currently miners vote for any particular change, and any associated parameters by posting their preference in the block header and extension block respectively. The hard-fork solution will measure a miner's support for any given change using new data provided in the extension section of a miner's block. + +With a defined range of potential support as defined by the new voting parameter, we can fine tune a balance of vote accuracy to data cost. This will enable us to increase the effective number of decimal places in a pool's vote should the network's hashrate grow to a point that pool censorship is taking place, and decrease it should the network reach an equilibrium of hashrate across pools such that all miner's votes are being counted accurately, and thus the data cost can be reduced. In the future, this parameter can be used to derive saturation targets for smart pools. + + +The greatest drawback to this solution is that: While it could be passed via a softfork, doing so would result in future chainsplitting, and as such a hardfork would likely be required. + +Please see the Node Settings section below for implementation method. + + +General Design +-------------- +This section pertains to the hard fork solution. + +Several parts of the Ergo node must be modified to allow for partial voting: + + 1. A new parameter that miners can vote on to later modify the maximum voting accuracy of pools. This parameter is used in several places to flatten our votes into the 1024 votes of a voting epoch. By including this value, we give miners the power to futher increase the accuracy of voting should network/pool hashrate grow to require it to avoid censorship, as well as to decrease the value should the network reach a saturation point with pools of equal hashrate that do not require such a high degree of accuracy, thereby reducing the data cost. + 2. The extension section of a miner's block may include additional information regarding the degree of their support in favour of any particular change measured on a scale with the maxVotingAccuracy's current value representing 100% in favour of change. + 3. The vote validation logic is modified to include the new data from the extension section. + +How pools decide to measure their votes internally (or if they do at all) is up to them, the only enforced logic with regards to voting accuracy is that they flatten their internal votes onto the network's current maxVotingAccuracy value. + +Node Settings +------------- +This section will detail how the hardfork solution can be implemented. + +Disclaimer: The accuracy of the below technical solution is my best guess on how this change could be implemented based on my amateur capacity to parse Scala. Review and testing has not been completed. + + + +source: +https://github.com/ergoplatform/ergo/blob/master/src/main/scala/org/ergoplatform/settings/VotingSettings.scala + + +This file decides how many votes are required to implement changes to the network for softforks and everyday changes respectively and how those votes are counted. The new parameter "maxVotingAccuracy" will be used that defines a minimum accuracy that votes may be and divide our votesCount and count by the value. This will ensure that if the maxVotingAccuracy value is modified by miner consensus, that future votes are automatically counted accordingly. + +```scala +package org.ergoplatform.settings +package org.ergoplatform.parameters + +case class VotingSettings(votingLength: Int, + softForkEpochs: Int, + activationEpochs: Int, + maxVotingAccuracy: Long, + version2ActivationHeight: Int, + version2ActivationDifficultyHex: String) { + + def softForkApproved(votesCount: Int): Boolean = votesCount / maxVotingAccuracy > votingLength * softForkEpochs * 9 / 10 + + def changeApproved(count: Int): Boolean = count / maxVotingAccuracy > votingLength / 2 +} +``` + +source: +https://github.com/ergoplatform/ergo/blob/master/src/main/scala/org/ergoplatform/nodeView/state/VotingData.scala + + + +The VotingData class will be modified to enable our logic: As voting is no longer a boolean, we should not be increasing the number of votes by '1' but by a value derived from the extension block. + + +We should no longer be counting (votes + 1) but rather checking the extension section for extension.VoteOpt[Long], and writing that value to the epochVotes.map. (this section needs works) + +```scala +... + +case class VotingData(epochVotes: Array[(Byte, Int)]) { + + def update(voteFor: Byte): VotingData = { + this.copy(epochVotes = epochVotes.map { case (id, votes) => + if (id == voteFor) id -> (votes + extension.VoteOpt[Long]) else id -> votes + }) + } + + override def canEqual(that: Any): extension.VoteColl[Long] = that.isInstanceOf[VotingData] + + override def equals(obj: scala.Any): extension.VoteColl[Long] = obj match { + case v: VotingData => v.epochVotes.sameElements(this.epochVotes) + case _ => false + } + +} + +object VotingData { + val empty = VotingData(Array.empty) +} + +object VotingDataSerializer extends ScorexSerializer[VotingData] { + + override def serialize(obj: VotingData, w: Writer): Unit = { + w.putUShort(obj.epochVotes.length) + obj.epochVotes.foreach { case (id, cnt) => + w.put(id) + w.putUInt(cnt) + } + } + + override def parse(r: Reader): VotingData = { + val votesCount = r.getUShort() + val epochVotes = (0 until votesCount).map {_ => + r.getByte() -> r.getUInt().toIntExact + } + VotingData(epochVotes.toArray) + } + +} +``` + + +source: +https://github.com/ergoplatform/ergo/blob/master/src/main/scala/org/ergoplatform/nodeView/state/ErgoStateContext.scala + + +As in the above file, we are no longer increasing votes by '1' but rather by a fraction of our new parameter maxVotingAccuracy as provided by miner data from their extension section. (this section needs works) +```scala +... + + extensionOpt match { + case Some(extension) if epochStarts => + processExtension(extension, header, forkVote)(state).map { processed => + val params = processed._1 + val extractedValidationSettings = processed._2 + val proposedVotes = votes.map(_ -> extension.VoteOpt[Int]) + val newVoting = VotingData(proposedVotes) + new ErgoStateContext(newHeaders, extensionOpt, genesisStateDigest, params, + extractedValidationSettings, newVoting)(ergoSettings) + } + case _ => + val newVotes = votes + val newVotingResults = newVotes.foldLeft(votingData) { case (v, id) => v.update(id) } + state.result.toTry.map { _ => + new ErgoStateContext(newHeaders, extensionOpt, genesisStateDigest, currentParameters, validationSettings, + newVotingResults)(ergoSettings) + } + } + }.flatten + } + +... + +``` +As we are no longer increasing votes by increments of 1, the logic to check for duplicate votes must be modified. (this section needs works) +```scala + * Check that non-zero votes extracted from block header are correct + */ + private def validateVotes(header: Header): ValidationState[ErgoStateContext] = { + val votes: Array[Byte] = header.votes.filter(_ != Parameters.NoParameter) + val epochStarts = header.votingStarts(votingSettings.votingLength) + val votesCount = votes.count(_ != Parameters.SoftFork) + val reverseVotes: Array[Byte] = votes.map(v => (-v).toByte) + + @inline def vs: String = votes.mkString("") + + ModifierValidator(validationSettings) + .payload(this) + .validate(hdrVotesNumber, votesCount <= Parameters.ParamVotesCount, InvalidModifier(s"votesCount=$votesCount", header.id, header.modifierTypeId)) + .validateSeq(votes) { case (validationState, v) => + validationState + .validate(hdrVotesDuplicates, votes.count(_ == v) == 1, InvalidModifier(s"Double vote in $vs", header.id, header.modifierTypeId)) + .validate(hdrVotesContradictory, !reverseVotes.contains(v), InvalidModifier(s"Contradictory votes in $vs", header.id, header.modifierTypeId)) + .validate(hdrVotesUnknown, !(epochStarts && !Parameters.parametersDescs.contains(v)), InvalidModifier(s"Incorrect vote proposed in $vs", header.id, header.modifierTypeId)) + } + } + +} +... +``` + + +source:https://github.com/ergoplatform/ergo/blob/master/src/main/scala/org/ergoplatform/settings/Parameters.scala + + +In the Parameters section we add our new adjustable parameter that is used to help calculate the accuracy of pool's votes. +```scala +class Parameters(val height: Height, + val parametersTable: Map[Byte, Int], + val proposedUpdate: ErgoValidationSettingsUpdate) + extends ErgoLikeParameters { + + import Parameters._ +... +/** + * Accuracy of voting data (as decimal) + */ +lazy val maxVotingAccuracy: Int = parametersTable(maxVotingAccuracyIncrease) + +... + } +``` +Below we must allow our maximum votes to obey our new logic so votes are not rejected. We allow the current value of maxVotingAccuracy to be added in place of '1', and multiply our vs.length by it to ensure our validation is sound. +```scala +private def padVotes(vs: Array[Byte]): Array[Byte] = { + val maxVotes = ParamVotesCount + MaxVotingAccuracy + if (vs.length * MaxVotingAccuracy < maxVotes) vs ++ Array.fill(maxVotes - vs.length * maxVotingAccuracy)(0: Byte) else vs + } +``` +Below we must modify our logic to allow votes to be cast as a fraction of our maxVotingAccuracy as relayed by miner's extension box (this section needs works): +```scala + def vote(ownTargets: Map[Byte, Int], epochVotes: Array[(Byte, Int)], voteForFork: extension.Opt[Int]): Array[Byte] = { + val vs = epochVotes.filter { case (paramId, _) => + if (paramId == Parameters.SoftFork) { + voteForFork + } else if (paramId > 0) { + ownTargets.get(paramId).exists(_ > parametersTable(paramId)) + } else if (paramId < 0) { + ownTargets.get((-paramId).toByte).exists(_ < parametersTable((-paramId).toByte)) + } else { + false + } + }.map(_._1) + padVotes(vs) + } + + def suggestVotes(ownTargets: Map[Byte, Int], voteForFork: extension.Opt[Int]): Array[Byte] = { + val vs = ownTargets.flatMap { case (paramId, value) => + if (paramId == SoftFork) { + None + } else if (value > parametersTable(paramId)) { + Some(paramId) + } else if (value < parametersTable(paramId)) { + Some((-paramId).toByte) + } else { + None + } + }.take(ParamVotesCount).toArray + padVotes(if (voteForFork) vs :+ SoftFork else vs) + } +``` +Adding information to allow for increasing or decreasing our new parameter, identifing default, minimum and step values: +```scala +... +object Parameters { +... + //Parameter identifiers +... + val MaxVotingAccuracyIncrease: Byte = 9 + val MaxVotingAccuracyDecrease: Byte = (-VotingAccuracyIncrease).toByte +... + val MaxVotingAccuracyDefault: Int = 10000 //0.00001 % of a pool's hashpower + val MaxVotingAccuracyStep: Int = 100 //Including a step can help to prevent an attack on miner governance + val MaxVotingAccuracyMin: Int = 1 //Changing this value to 1 would disable partial voting +... + val DefaultParameters: Map[Byte, Int] = Map( + ... + MaxVotingAccuracyIncrease -> VotingAccuracyDefault, + BlockVersion -> 1 + ) +... + val parametersDescs: Map[Byte, String] = Map( + ... + MaxVotingAccuracyIncrease -> "Maximum allowed voting accuracy (A vote 100% in favour should fill this value)" + ) +... + val stepsTable: Map[Byte, Int] = Map( + ... + MaxVotingAccuracyIncrease -> MaxVotingAccuracyIncreaseStep, + ) +... +val minValues: Map[Byte, Int] = Map( + ... + MaxVotingAccuracyIncrease -> MaxVotingAccuracyIncreaseMin, + ) +... + implicit val jsonEncoder: Encoder[Parameters] = { p: Parameters => + Map( + "height" -> p.height.asJson, + "blockVersion" -> p.blockVersion.asJson, + ... + "maxVotingAccuracy" -> p.votingAcccuracy.asJson + ).asJson + } + +``` + +The No-fork Solution +------------------------ +Using the Smart Pools, Sub Pools, and SNISPs designed by GetBlok.io we can create a system wherein a central pool operator can host a pool that both enables smooth mining rewards, and enables it's miners to vote directly on daily and foundational changes to the network. + +GetBlok.io's smartpool and subpool contracts are used to create Smart Pools that contain a liquid number of Sub Pools with the following requirements: + +-Sub Pools within the smartpool use SNISPs both internally and across the entire Smart Pool + +-Sub Pools share block rewards with each other, using the share data across all subpools within the system to calculate and payout subpools and their miners + +-Each miner in the Smart Pool is awarded a box by their Sub Pool's payment contract that enables them to sign a contract managed by the Smart Pool with their preferred voting parameters + +-Each Sub Pool has it's own node, (bootstrapped by the pool operator's default) + +-The pool operator's default node votes for no changes to the network + +-When a miner within the system indicates their desire to vote for any changes using the above mentioned voting contract, their future shares are redirected to a Sub Pool (and node) matching their respective preference + +-If no Sub Pools and respective nodes exist within the Smart Pool matching the miner's preference, a new Sub Pool is created by smart contract and a new node is create by the Pool Operator matching the miner's voting preference. (The offchain logic for this crucial step has not yet been built. It can designed by central pool operators and still be compatible with this design in various ways: manually; using containers like docker; using virtual machines; using Flux nodes; using AWS, etc.) + +-After confirmation of the first block of a voting epoch, where any potential changes are proposed, Sub Pools and their nodes voting for any changes that are not the proposed change are consolidated back into the default Sub Pool and node indicating a 'no' vote. + + + By using Smart Pools to split rewards fairly between Sub Pools managed by their own respective nodes with their own respective voting preferences, not only will all miners within the system will see on average the same rewards as they would mining to a tradtional pool, but they will also have the power to vote on changes to the protocol while supporting their sibling pools with different votes. + +While this solution mitigates traditional pool censorship and avoids a hardfork, it has several drawbacks: + +-Because voting on Ergo takes places over epochs and a fixed number of votes will see the network upgraded, there exists incentive for miners to consolidate hashrate into larger nodes supporting their voting preference during voting periods. While this solution does at least grant minority voters a method for hashrate/vote consolidation where one may not otherwise exist, they are still at a disadvantage when compared to the hard-fork solution: The hardfork solution allows voting miners to vote in a pool representing a greater percentage of the network's hashpower, maximizing the chance that their vote is counted during the voting epoch. + +-This solution increases the hardware overhead for central pool operators; as they will need to host at least one additional node, and an increasing number of nodes up to the hard limits of the sub pooling system, or the number of their miners indicating support for different proposals leading up to the voting epoch. This is feasible up to a point for larger pools, but reduces the ability for smaller pool operators to compete against those with the hardware capacity should miners grow to value their governance power enough that a majority of miners are mining to Smart Pools that enable governance in this way. (This can be mitigated in the future by decentralized solutions that reduce the need for central pool operators.) + +A tool is being built for Ergohack V called Piazo that will enable proof for the above claims that the smart pool solution is prone to further censorship. + +Voting for the Fork +------------------------ +(Placeholder) + +Activation Details +------------------ +(Placeholder) + +API Methods Changed +------------------- +(Placeholder) + +Testnet Data +------------ + +(Placeholder) + +Mainnet Data +------------ + +(Placeholder) + +References +---------- +https://ergoplatform.org/en/blog/2021-04-26-the-ergo-manifesto/ "The Ergo Manifesto" Ergo Foundation + +https://storage.googleapis.com/ergo-cms-media/docs/whitepaper.pdf "Ergo: A Resilient Platform For Contractual Money" Ergo Developers + +https://eprint.iacr.org/2020/044.pdf "Bypassing Non-Outsourceable Proof-of-Work Schemes Using Collateralized Smart Contracts" Alexander Chepurnoy, Amitabh Saxena + +https://github.com/ergoplatform/ergo + +https://github.com/GetBlok-io/ergo-smartpooling-contracts + +https://github.com/GetBlok-io/Subpooling + +https://stackoverflow.com/questions/14822317/logic-operators-for-non-boolean-types-in-scala + +"Succinct, Non-Interactive Share Proofs" Kirat Singh + +https://github.com/deadit/paizo