Try   HackMD

Requirements fullfiled by this proposal

  • allow vote directly over an Ethereum census3 made out of addresses and balances
  • allow vote using Metamask or any other Ethereum signer
  • do not requiere registration in advance for the user
  • allow the user to specify its own custom voting weight while smaller or equal than the balance
  • support on browsers and mobile devices

Proposal

The process of anonymous voting for Ethereum DAOs using Vocdoni involves the use of an identity commitment registry, which securely stores a secret owned by a Vocdoni user in a merkle tree. This secret is generated through a cryptographic process that employs a Hash function with at least one parameter considered secret for the user.

The specific formula for generating the commitment key, known as the secretIdentityKey, is as follows:

SecretIdentityKey = Hash(address + signature + password)

To gain a deeper understanding of the components involved, let's examine each one:

  • address: This refers to the Ethereum address of the user participating in the DAO voting process.
  • signature: An ECDSA signature generated over a specific payload, which remains constant for all users. It is crucial to keep this signature private and not disclose it.
  • password: An optional password provided by the user to enhance the security of the commitment key.

The secret identities are stored in a merkle tree as part of the Vochain state. The blockchain logic ensures that only one key is registered per account address and that the key has been created by the owner of that particular address. The blockchain verifies the ECDSA signature before adding the commitment identity to maintain the integrity of the system.

The secret identity plays a crucial role in proving ownership of an address anonymously within a zkSnark. To construct the proof, the user must possess the deterministic signature and the password, which are used as private inputs for the zk-SNARK circuit.

To prevent double voting and uphold the integrity of the voting process, a nullifier is computed by hashing the secret components together with the election identifier. The nullifier calculation follows this formula:

nullifier = hash(signature + password + electionID)

By incorporating the election identifier into the nullifier, each user can reproduce their own nullifier. However, any attempt to produce a second zkSnark proof for the same identity will result in the same nullifier. This mechanism effectively prevents double voting and ensures the authenticity of the voting process.

SIK merkle tree

The secret identity key (SIK) merkle tree is a global data structure living inside the Vocdoni Vochain that stores a relation between an Ethereum address and its SIK. For each user only one secret identity can exist.

The tree is composed by:

  • index: ethereum address
  • value: secret identity key

The address (index) routes the path through the tree until its SIK (value). This mechansim guarantees the uniqueness of a SIK per address.

SikRoot

SIK1

SIK2

Addr1.1

Addr2.1

Addr1.2

Addr1.3

Addr2.2

Addr2.3

The Vochain transaction for adding a new identity proves the ownership of the address (by signing the transaction content) and provides the secret identity key.

Census merkle tree (census3)

The Census merkle tree is the census constructed by the Vocdoni's Census3 service out of the Ethereum data by quering a Web3 endpoint.

The purpose of the Census Merkle Tree is to create an immutable and verifiable representation of the token holders within the DAO on a specific block. It enables efficient and secure verification of an address's presence in the DAO's token holder set.

Vochain verification

The Vocdoni Vochain serves as a verifier for the SIK merkle tree construction, ensuring its correctness.

Additionally, the Vochain verifies the voting proof submitted by DAO users by:

  • Validating the accuracy of the zkSnark proof.
  • Verifying that the ciMkroot is one of the last merkle roots stored in the chain.
  • Confirming that the electionId corresponds to an ongoing election.
  • Ensuring that the censusMkRoot matches the one set during election creation.

By conducting these verifications, the Vochain maintains the integrity and reliability of the voting process within the Vocdoni ecosystem.

zkSNARK circuit

The circuit has been designed to be zkSnark friendly and reduce the number of constraints to the minimum. To this end we do not require any secp256k1 cryptography operation.

The zk-SNARK circuit proves that the voter is eligible to vote (its Ethereum address is included in the Census Merkle Tree), that they know the secret inputs (signature and password) corresponding to their CIK, and that they have not voted more than their balance allows. It also ensures that the voter cannot vote more than once in the same election by generating a unique nullifier for each vote. All of this is done without revealing the voter's Ethereum address, preserving the anonymity of the vote.

                                                            +---------------------+                                         
                                                            |                     |                                         
                                                            |                     |                                         
                                                            |     SMT verifier    |                                         
   priv:  SIKmkProof--------------------------------------->|                     |                                         
                                                            |                     |                                         
   pub:   SIKmkRoot---------------------------------------->| value key           |                                         
                                                            +---------------------+                                         
                                    +----------+                ^    ^                                                      
   priv:  password---------|------->|          |                |    |                                                      
   priv:  signature--------|--|---->|   Hash   |----------------+    |                                                      
                           |  |     |          |                     |                                                      
                           |  |     +----------+                     |                                                      
                           |  |        ^                             |                                                      
                           |  |        |                             |                                                      
   priv:  ethAddress-------|--|--------+-----------------------------+                                                      
                           |  |        |                                                                                    
                           |  |        |                    +---------------------+                                         
                           |  |        +------------------->|key                  |                                         
   priv:  censusMkProof----|--|---------------------------->|                     |                                         
                           |  |                             |    SMT verifier     |                                         
   pub:   censusMkRoot-----|--|---------------------------->|                     |                                         
                           |  |                             |                     |                                         
   priv:  censusWeight-----|--|---------------------------->|value                |                                         
                           |  |                      |      +---------------------+                                         
                           |  |                      |                                                                      
                           |  |      +----------+    |                                                                      
                           |  +----->|          |    |   +---------+                                                        
                           |         |   Hash   |----|-->| equals  |                                                        
                           +-------->|          |    |   +---------+                                                        
                                     +----------+    |        ^                                                             
                                        ^            |        |                                                             
   pub:   electionId--------------------+            |        |                                                             
                                                     |        |                                                             
   pub:   nullifier----------------------------------|--------+                                                             
                                                     |                                                                      
                                                     v                                                                      
                                              +---------------+                                                             
   priv:   voteWeight------------------------->| less or equal |                                                             
                                              +---------------+                                                             

Tech specification

The state database needs to be modified in the following way.

3rd level

2nd level

1st level

stateRoot

MainTree

Extra

key:string
val:bytes

Validators

key:address
val:models.Validator

Accounts

key:address
val:models.Account

Faucet

key:addr+nonce
val:nul

Elections

key:processID
val:models.Process

PreRegisterCensus

key:voterID
val:weight

PreregisterNullifiers

key:initialNullifier
val:weight

Votes

key:voteId
val:models.StateDBVote

SIKs

key:addr
val:SIK

Transactions

The Secret Identity Key (SIK) can be managed through the SetAccount transaction. There are three transaction types (txTypes) that allow for modifications to the SIK:

  • CREATE_ACCOUNT

    • During account creation, a new SIK can be specified.
    • The only required validations are basic format checks.
  • DEL_ACCOUNT_SIK

    • This transaction will fail if the account does not have a SIK.
    • Upon deletion of a SIK, the SIK tree is modified as follows:
      1. The address is retained as the index in the Merkle Tree.
      2. The SIK leaf value is replaced by the block number at which adding a new SIK will be allowed, following the format: [28 bytes of 0's] + [4 bytes blockchain Height LittleEndian]
      3. The block number set needs to be equal to the highest endblock of all the ongoing elections (to allow double voting)
  • SET_ACCOUNT_SIK

    • This transaction will fail if the account address already has an active SIK.
    • If there is a leaf on the Merkle Tree (MT) for the address, the following checks are performed:
      1. The initial 28 bytes of the leaf must be zero.
      2. The remaining 4 bytes, when converted to a LittleEndian uint32, must satisfy the condition: height < current_height.
Hysteresis

The term "Height Hysteresis" is defined as the number of blocks for which the SIK Merkle root remains valid relative to the current height.

For example, Height Hysteresis could be defined as a constant value of 360. So at 10 seconds/blocks, it implies a time window of 1h.

The hysteresis mechanism is used to provide a buffer or grace period during which the SIK Merkle root remains valid even after a change in the blockchain height. This is important because it allows for a smoother transaction processing experience.

On the stateDB extra tree we store a new leaf:

SIK reset

When a user deletes his or her SIK, the leaf of the SIKMerkleTree identified by the user address will contain the height when it was deleted.

In this way, this SIK only can be setted again if the height that the leaf contains is lower than any ongoin process startBlock (plus the hysteresis*):

    delSIKTx => "address": heightOnDelete
    
    setSIKTx <=> heightOnDelete < min(onGoingProccessStartBlock) + hysteresis

* Hysteresis: the number of past blocks on which we allow a SIK root to be valid when sending votes

To achieve this, we need to store at nostate database the list of current ongoing processes and its startBlock:

    {
        "startBlock1": ["processID1", "processID2"],
        "startBlock2": ["processID3"]
    }

To keep this list updated:

  • On EndProcess, we just remove the the processID entry (we can fetch the KV entry by startBlock).
  • On AddProcess, we add the processID entry by its startBlock.
    • If a process starts with startBlock=0 and status=PAUSED, we add it to the list with current height (we need to check code for this casuistic)

Hysteresis root list

The hysteresis is the number of past blocks on which we allow a SIK root to be valid when sending votes. Required to verify proofs of vote generated with old but valid SIK merkle tree roots.

To achieve this, we need to store at nostate database the list of valid SIK roots:

    {
        "blockNumber2": "sikRoot2",
        "blockNumber1": "sikRoot1",
    }

and define the number of blocks the hysteresis means:

    const hysteresis = 32;

To keep this list updated:

  • When a new sikRoot is generated, the sikRoot's from an older block than the current block minus the hysteresis blocks will be deleted:
    • If exists a sikRoot for the minimun hysteresis block number (currentBlock - hysteresis), just remove all the roots with a lower block number.
    • If it does not exist, remove all roots with a lower block number except for the next lower sikRoot. It is becouse it still being validate for a period.

Example:

    Hysteresis: 10 blocks

    Current sikRootArchive:
    {
        "1": "0x8923f3a211",
        "10": "0xasb23c3128"
    }

====> New sikRoot!: "15": "0xasb23c3128"
    
    New sikRootArchive: 
    {
        "1": "0x8923f3a211", // keep it because it still valid in block range (5-15)
        "10": "0xasb23c3128",
        "15": "0xasb23c3128"
    }
    
====> New sikRoot!: "20": "0xf21309881"

    New sikRootArchive: 
    {
        "10": "0xasb23c3128", // keep it because it the last valid block in range (10-20)
        "15": "0xasb23c3128",
        "20": "0xf21309881"
    }