On-Chain Voting Results for MIPs
Granola Systems is a software consultancy and Mina ecosystem partner. We make winning teams using our expertise in leadership, DevOps, Web3, distributed systems, functional programming, and data engineering. Granola has developed a dashboard for displaying on-chain MIP 3 Voting Results MIP 4 Voting Results. We operate an archive node and do regular staking ledger dumps.
This article details the calculation of Mina's on-chain voting results. Equipped with the tools and knowledge, you can independently verify our code, logic, and results. Public blockchains like Mina Protocol are truly remarkable and equally as complex! 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!
We will describe in detail how to calculate the results of Mina's on-chain stake-weighted voting!
At a high level, we will
-
Obtain the next staking ledger of the next epoch
- The staking ledger of epoch 56 contains the necessary info about delegations
- Available circa block 290 of the epoch
-
Calculate aggregated voter stake as a float (Rust
f64
)- Sum all delegations to each voting public key
- 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 yse/no weight
- Sum yes/no vote stakes
- Divide by the total voting stake
There are several appropriate levels of engagement with this documentation. Readers are welcome to (from the highest level of technical knowledge/effort required to the least):
- Run a local archive node (the way Granola does it for the dashboard)
- Query local PostgreSQL database for all relevant ledger and transaction data
- Export next staking ledger and hash from local mina daemon
- Get transaction data via public GraphQL queries
- Download staking ledger from Granola-Team/mina-ledger, zkvalidator/mina-graphql-rs, or Mina Explorer's Data Archive
- Write an independent program in your favorite language implementing the calculation steps
- Just try a few queries
- Generate a report using the
mina_voting
utility - Simply read along and learn
- Export from a local daemon
- Granola-Team/mina-ledger
- zkvalidator/mina-graphql-rs
- Mina Explorer Data Archive
- Run your own archive node
- Mina Explorer GraphiQL GUI
We calculate the results of 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.
-
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: 55}}}, limit: 1) { protocolState { consensusState { nextEpochData { ledger { hash } } epoch } } } }
response = { 'data': { 'blocks': [ { 'protocolState': { 'consensusState': { 'epoch': 55, 'nextEpochData': { 'ledger': { 'hash': 'jw8dXuUqXVgd6NvmpryGmFLnRv1176oozHAro8gMFwj8yuvhBeS' } } } } } ] } }
Extract the value corresponding to the deeply nested
hash
keyresponse['data']['blocks'][0]['protocolState']['consensusState']['nextEpochData']['ledger']['hash']
-
Now that we have the appropriate ledger hash, we can acquire the corresponding staking ledger, in fact, the next staking ledger of epoch 55, via
wget https://raw.githubusercontent.com/Granola-Team/mina-ledger/main/mainnet/jw8dXuUqXVgd6NvmpryGmFLnRv1176oozHAro8gMFwj8yuvhBeS.json -O path/to/ledger.json
You can use any of the following sources (extra credit: use them all and check diffs)
- Mina Explorer’s data archive
- Zk Validator's mina-graphql-rs repo
- Granola’s mina-ledger repo
-
If you’re running a local daemon, you can export the next staking ledger (while we are in epoch 45) 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!
-
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 = float(account['balance']) # pk delegates if pk != dg: delegators.add(pk) try: agg_stake[dg] += bal except: agg_stake[dg] = bal
-
Drop delegator votes
for d in delegators: try: del agg_stake[d] except: pass
-
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.
- Multiple data sources
-
Obtain the unique voting transactions
-
A vote is a transaction satisfying:
-
kind = PAYMENT
-
source = receiver
-
Valid
memo
field (eithermip3
orno 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. A priori, 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-01-04T16:00:00Z", dateTime_lte: "2023-01-14T08:30:00Z"}, limit: 7140) { blockHeight } }
The max number of slots, hence blocks, in an epoch is
7140
. The response in includes block heights213195
to216095
{ "data": { "blocks": [ { "blockHeight": 216095 }, ... { "blockHeight": 213195 } ] } }
-
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": 216063, "memo": "E4Yxu8shUhP1SMV5fUoGZb4sqEPREUCLErpYVJMQD1pY5iuocbibr", "nonce": 41977, "receiver": { "publicKey": "B62qqJ1AqK3YQmEEALdJeMw49438Sh6zuQ5cNWUYfCgRsPkduFE2uLU" }, "source": { "publicKey": "B62qnXy1f75qq8c6HS2Am88Gk6UyvTHK3iSYh4Hb3nD6DS2eS6wZ4or" } }, ... { "blockHeight": 216063, "memo": "E4YM2vTHhWEg66xpj52JErHUBU4pZ1yageL4TVDDpTTSsv8mK6YaH", "nonce": 3, "receiver": { "publicKey": "B62qjt1rDfVjGX6opVnLpshRigH5U6UFjyMNYdWCjo99im2v7VrzqF6" }, "source": { "publicKey": "B62qpZMGRKrse3mVxf3SNMfzWh5c4TM2PfvgL98oVadwNWb6S1tJ8Te" } } ] } }
Notice the base58 encoded
memo
field - Concatenate transactions for all canonical blocks in the voting period
-
Filter the votes
-
memo.lower()
exactly equal tomip3
orno 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.
-
A vote is a transaction satisfying:
- 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 a disagreeing vote
- Divide yes/no vote stakes by the total voting stake, as a float in Python, f64 in Rust
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: https://gist.github.com/trevorbernard/ec11db89bb9079dd0a01332ef32c0284 MIP4: https://gist.github.com/trevorbernard/928be21e8e1d9464c3a9b2453d9fd886
Granola's MIP3 results dashboard
Granola's MIP4 results dashboard
MIPs are a substantial addition to Mina's ever-evolving suite of on-chain governance functionalities. This is a huge milestone which advances the community and decentralization of the protocol. Granola is incredibly honored to be a part of the technical implementation of such a major development in the Mina ecosystem.