On-Chain Voting Results for MIPs
This article details the calculation of Mina’s on-chain voting results and how to add new MIPs to the On-Chain Voting (OCV) Dashboard for an upcoming voting period.
Equipped with the tools and knowledge, anyone can independently verify the code, logic, and results. We always strive for correctness in our code and exhaustively test.
All the code for this project is open source ❤️ and available on GitHub.
Find an issue or a bug? Have a question or suggestion? We’d love to get your feedback!
- How to add a new MIP
- Maintaining the lifecycle of a MIP in the OCV
- Calculation of Mina’s On-Chain Voting Results
- Credits
Adding a MIP in the OCV will ensure that the MIP's progress is visible and that the votes are counted and weighed correctly. To add a new MIP, please follow the following steps:
- Go to the MIP list file in the OCV repo
- Click on the pencil icon to edit the file
- Add a new entry to the list of proposals, following the format of the existing entries
- the
idshould be the next available integer - the
titleshould be a descriptive title of the MIP - the
keyshould be the MIP number (e.g.MIP5) - This will be the memo used for voting this MIP - the
epochshould be the epoch number when voting will take place for this MIP. Check here the current epoch. - the
start_timeandend_timeshould be the timestamps when voting starts and ends for this MIP - the
urlshould be a link to the MIP's proposal on the Mina Protocol MIPs repository - the network should be
mainnetordevnet - the
ledger_hashmust be the ledger hash of the epoch after the voting epoch (e.g. for voting in epoch 53, use the ledger hash of epoch 55). Check how to obtain the staking ledger section for more details - the
is_completefield has to be set tofalsewhen adding a new MIP
- the
- Once you have added the new entry, scroll down to the bottom of the page and click on the "Propose changes" button
- In the next page, add a descriptive title and description for your changes, then click on the "Create pull request" button
- Once the pull request is reviewed and approved, it will be merged into the main branch and the new MIP will be added to the OCV dashboard
This section describes the actions needed to be taken during the lifecycle of a MIP and how the statuses are updated in the OCV dashboard.
| Step | Status | Description |
|---|---|---|
| 1 | Pending | MIP is added to repo |
| 2 | In Progress | Voting period active |
| 3 | In Review | Voting ended, verifying results |
| 4 | Complete | Results verified and published |
- A MIP has reached finalization stage and is ready for on chain voting, so it is added to the MIP list file in the OCV repo
- After the voting period has started, the MIP status will change to "In Progress"
- Once the voting period has ended, the MIP status will change to "In Review". At this point the results are being verified.
- The voting results will be verified and calculated using the steps described in the Calculation of Mina’s On-Chain Voting Results section
- Once the results have been verified, a new pull request will be created to update the MIP
is_completefield totrue. At this point, the MIP status will change to "Complete" and the results will be displayed on the Dashboard.
We will describe in detail how to calculate the results of Mina’s on-chain stake-weighted voting!
Note: This tutorial uses epoch 55 and specific hardcoded values as a concrete example to demonstrate the voting calculation process. You'll need to adjust these values for your actual use case. At a high level, we will
- Obtain the next-staking-ledger of the next voting epoch
- The results are calculated using the staking ledger of epoch 55
- Calculate aggregated voter stake
- Sum all delegations to each voting public key minus any overriding votes.
- Voter stake weight is calculated with respect to the total voting stake
- Obtain transaction data for the voting period, need start and end times
- Filter the voting (self) transactions, i.e. those with source = receiver
- Base58 decode the memo field of all votes
- Calculate yes/no weight
- Sum yes/no vote stakes
- Divide by the total voting stake
As mentioned above, this tutorial uses specific historical votes to demonstrate the process. We'll calculate the results for MIP3 and MIP4 voting:
-
MIP3 Start: 5/20/23 at 6:00 AM UTC (Epoch 53, Slot 2820)
-
MIP3 End: 5/28/23 at 6:00 AM UTC (Epoch 53, slot 6660)
-
MIP4 Start: 5/20/23 at 6:00 AM UTC (Epoch 53, Slot 2820)
-
MIP4 End: 5/28/23 at 6:00 AM UTC (Epoch 53, slot 6660)
Data Value Epoch 53 Keyword MIP3, MIP4 Start time May 20, 2023 06:00 UTC End time May 28, 2023 06:00 UTC
Since we are calculating the results for MIP3 and MIP4 voting (epoch 53), we need the next-staking-ledger of the next epoch, i.e. the staking ledger of epoch 55.
a. If you are not running a daemon locally, you will first need the ledger hash. Use the query
query NextLedgerHash {
blocks(query: {canonical: true, protocolState: {consensusState: {epoch: 54}}}, limit: 1) {
protocolState {
consensusState {
nextEpochData {
ledger {
hash
}
}
epoch
}
}
}
}
response = {
"data": {
"blocks": [
{
"protocolState": {
"consensusState": {
"epoch": 54,
"nextEpochData": {
"ledger": {
"hash": "jw8dXuUqXVgd6NvmpryGmFLnRv1176oozHAro8gMFwj8yuvhBeS"
}
}
}
}
}
]
}
}
Extract the value corresponding to the deeply nested hash key
response['data']['blocks'][0]['protocolState']['consensusState']['nextEpochData']['ledger']['hash']
b. Now that we have the appropriate ledger hash, we can acquire the corresponding staking ledger, in fact, the next staking ledger of epoch 54. You can use any of the following sources (extra credit: use them all and check diffs)
- Mina Explorer’s data archive
- If you’re running a local daemon, you can export the next staking ledger (while we are in epoch 54) by
mina ledger export next-staking-ledger > path/to/ledger.json
and confirm the hash using
mina ledger hash --ledger-file path/to/ledger.json
This calculation may take several minutes!
a. Calculate each voter's stake from the staking ledger. Aggregate all delegations to each voter (by default, an account is delegated to itself)
agg_stake = {}
delegators = set()
for account in ledger:
pk = account['pk']
dg = account['delegate']
bal = Decimal(account['balance'])
# pk delegates
if pk != dg:
delegators.add(pk)
try:
agg_stake[dg] += bal
except:
agg_stake[dg] = bal
b. Drop delegator votes
for d in delegators:
try:
del agg_stake[d]
except:
pass
c. Now agg_stake is a Python dict containing each voter's aggregated stake
To obtain all MIP3 and MIP4 votes, we need to get all transactions corresponding to the voting period (votes are just special transactions after all). It would be nice to be able to prefilter the transactions more and only fetch what is required, but since memo fields are base58 encoded and any capitalization of the keyword is valid, prefiltering will be complex and error-prone.
a. Multiple data sources
b. Obtain the unique voting transactions
-
A vote is a transaction satisfying:
kind = PAYMENTsource = receiver- Valid memo field (either mip3 or no mip3)
-
Fetch all transactions for the voting period
- To avoid our requests getting too big and potentially timing out, we will request the transactions from each block individually
- Block production varies over time; sometimes many blocks are produced in a slot, sometimes no blocks are produced. As priority, we do not know the exact block heights which constitute the voting period. We fetch all canonical block heights for the voting period, determined by the start and end times
query BlockHeightsInVotingPeriod { blocks(query: {canonical: true, dateTime_gte: "2023-05-20T6:00:00Z", dateTime_lte: "2023-05-28T06:00:00Z"}, limit: 7140) { blockHeight } }The max number of slots, hence blocks, in an epoch is 7140. The response in includes block heights 253078 to 255481
{ "data": { "blocks": [ { "blockHeight": 255481 }, ... { "blockHeight": 253078 } ] } }- For each canonical block height in the voting period, query the block’s PAYMENT transactions (votes are payments)
query TransactionsInBlockWithHeight($blockHeight: Int!) { transactions(query: {blockHeight: $blockHeight, canonical: true, kind: "PAYMENT"}, sortBy: DATETIME_DESC, limit: 1000) { blockHeight memo nonce receiver { publicKey } source { publicKey } } }where $blockHeight is substituted with each of the voting period’s canonical block heights (automation is highly recommended). Again, we include a limit which far exceeds the number of transactions in any block so we don’t accidentally get short-changed by a default limit. This process will take several minutes if done sequentially. Performance improvements are left as an exercise to the reader.
For example, the response for block 216063
{ "data": { "transactions": [ { "blockHeight": 255481, "memo": "E4YM2vTHhWEg66xpj52JErHUBU4pZ1yageL4TVDDpTTSsv8mK6YaH", "nonce": 367551, "receiver": { "publicKey": "B62qjYanmV7y9njVeH5UHkz3GYBm7xKir1rAnoY4KsEYUGLMiU45FSM" }, "source": { "publicKey": "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy" } }, ... { "blockHeight": 255481, "memo": "E4YM2vTHhWEg66xpj52JErHUBU4pZ1yageL4TVDDpTTSsv8mK6YaH", "nonce": 105533, "receiver": { "publicKey": "B62qkiF5CTjeiuV1HSx4SpEytjiCptApsvmjiHHqkb1xpAgVuZTtR14" }, "source": { "publicKey": "B62qoXQhp63oNsLSN9Dy7wcF3PzLmdBnnin2rTnNWLbpgF7diABciU6" } } ] } }Notice the base58 encoded memo field 4. Concatenate transactions for all canonical blocks in the voting period
- Filter the votes
- memo exactly equal to MIP3 or no MIP3
- source = receiver (self transaction)
- The memo field is base58 encoded
- If there are multiple votes associated with a single public key, only the latest vote is counted; latest being defined:
- For multiple votes from the same account across several blocks, take the vote in the highest block.
- For multiple votes from the same account in the same block, take the vote with the highest nonce.
- Sum all aggregated voter stake to get the total voting stake
- For each delegate, start with their total stake, and subtract the balances of accounts that delegate to them with an overriding vote
- Divide yes/no vote stakes by the total voting stake
Find all votes made by a delegating account, and subtract their account balance from the final voting stake if they disagree with their delegate
delegating_stake = {}
delegating_votes = {}
for vote in votes:
if vote.pk in delegators:
delegating_stake[vote.pk] = accounts[vote.pk]['balance']
delegating_votes[vote.pk] = vote.memo
for vote in delegating_votes
delegate_vote = votes[accounts[pk]['delegate']]
if against(delegate_vote) and for(vote) and pk not in delegating_votes:
no_stake -= delegating_stake[vote.pk]
else if for(delegate_vote) and against(vote) and pk not in delegating_votes:
yes_stake -= delegating_stake[vote.pk]
Check agreement with the voting results dashboard and/or @trevorbernard’s verification scripts
MIP3 and MIP4 Voting Results
This tutorial is based on the original MIP voting implementation by the Granola Team. Their work on on-chain governance functionality represents a significant milestone in advancing community participation and decentralization of the Mina ecosystem.