Skip to content

Latest commit

 

History

History
474 lines (404 loc) · 15.5 KB

voting-results-instructions.md

File metadata and controls

474 lines (404 loc) · 15.5 KB

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!

Calculation of Mina's On-Chain Voting Results

We will describe in detail how to calculate the results of Mina's on-chain stake-weighted voting!

Overview

At a high level, we will

  1. 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
  2. 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
  3. Obtain transaction data for the voting period, need start and end times
  4. Filter the voting (self) transactions, i.e. those with source = receiver
  5. Base58 decode the memo field of all votes
  6. Calculate yse/no weight
    • Sum yes/no vote stakes
    • Divide by the total voting stake

How to use this document

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):

Where to obtain the data

Staking ledgers

Blocks and transactions

Results Calculation Instructions

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

Calculation steps

Obtain staking ledger

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.

  1. 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 key

    
    response['data']['blocks'][0]['protocolState']['consensusState']['nextEpochData']['ledger']['hash']
    
  2. 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)

    1. Mina Explorer’s data archive
    2. Zk Validator's mina-graphql-rs repo
    3. Granola’s mina-ledger repo
    4. 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!

Aggregate voting stake

  1. 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
    
  2. Drop delegator votes
    
    for d in delegators:
        try:
            del agg_stake[d]
        except:
            pass
    
  3. Now agg_stake is a Python dict containing each voter's aggregated stake

Obtain and parse votes

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.

  1. Multiple data sources
    1. Run a local archive node
    2. Mina Explorer has BigQuery , GraphQL , and REST APIs
  2. Obtain the unique voting transactions
    1. A vote is a transaction satisfying:
      1. kind = PAYMENT
      2. source = receiver
      3. Valid memo field (either mip3 or no mip3)
    2. Fetch all transactions for the voting period
      1. To avoid our requests getting too big and potentially timing out, we will request the transactions from each block individually
      2. 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 heights 213195 to 216095

        
        {
          "data": {
            "blocks": [
              {
                "blockHeight": 216095
              },
              ...
              {
                "blockHeight": 213195
              }
            ]
          }
        }
        
      3. 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

      4. Concatenate transactions for all canonical blocks in the voting period
    3. Filter the votes
      1. memo.lower() exactly equal to mip3 or no mip3
      2. source = receiver (self transaction)
    4. The memo field is base58 encoded
    5. If there are multiple votes associated with a single public key, only the latest vote is counted; latest being defined:
      1. For multiple votes from the same account across several blocks, take the vote in the highest block.
      2. For multiple votes from the same account in the same block, take the vote with the highest nonce.

Calculate weighted voting results

  1. Sum all aggregated voter stake to get the total voting stake
  2. For each delegate, start with their total stake, and subtract the balances of accounts that delegate to them with a disagreeing vote
  3. Divide yes/no vote stakes by the total voting stake, as a float in Python, f64 in Rust

Adjust Votes and Voting Stake with Non-Delegating Voters

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

Vote Verification Scripts

MIP3: https://gist.github.com/trevorbernard/ec11db89bb9079dd0a01332ef32c0284 MIP4: https://gist.github.com/trevorbernard/928be21e8e1d9464c3a9b2453d9fd886

MIP3 and MIP4 Voting Results

Granola's MIP3 results dashboard

Granola's MIP4 results dashboard

Conclusion

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.