Browse Source

example/dao: re-add python dao schema

Dastan-glitch 3 years ago
parent
commit
32456baf7a
4 changed files with 1998 additions and 0 deletions
  1. 46 0
      example/dao/schema/classnamespace.py
  2. 158 0
      example/dao/schema/crypto.py
  3. 1463 0
      example/dao/schema/main.py
  4. 331 0
      example/dao/schema/money.py

+ 46 - 0
example/dao/schema/classnamespace.py

@@ -0,0 +1,46 @@
+from types import SimpleNamespace
+
+class ClassNamespace(SimpleNamespace):
+    def __init__(self, dic=None):
+        if dic is None:
+            return
+        # if type(dic) is dict:
+        for key in dic:
+            setattr(self, key, self.envelop(dic[key]))
+        # else:
+        #     raise CatalogError("ClassNamespace AIUTO!")
+
+    def envelop(self, elem):
+        if type(elem) is dict:
+            return ClassNamespace(elem)
+        elif type(elem) is list:
+            return [self.envelop(x) for x in elem]
+        else:
+            return elem
+
+        # if d is not None:
+        #     for key in d:
+        #         if type(d[key]) is dict:
+        #             setattr(self, key, ClassNamespace(d[key]))
+        #         else:
+        #             setattr(self, key, d[key])
+
+    def __contains__(self, x):
+        return x in self.__dict__
+
+    def __json__(self, x):
+        return self.__dict__
+
+    def copy(self):
+        return self.__dict__.copy()
+
+    def classcopy(self):
+        dummy = ClassNamespace()
+        dummy.__dict__.update(self.__dict__)
+        return dummy
+
+    def dictcopy(self):
+        return self.__dict__.copy()
+
+    def update(self, oth):
+        self.__dict__.update(oth.__dict__)

+ 158 - 0
example/dao/schema/crypto.py

@@ -0,0 +1,158 @@
+import hashlib
+import random
+
+def ff_inv(a, p):
+    a %= p
+
+    # extended euclidean algorithm
+    # ps + at = 1
+    t = 0
+    new_t = 1
+    r = p
+    new_r = a
+
+    while new_r != 0:
+        quotient = r // new_r
+        t, new_t = new_t, t - quotient * new_t
+        r, new_r = new_r, r - quotient * new_r
+
+    assert r == 1
+    if t < 0:
+        t += p
+
+    return t
+
+class EllipticCurve:
+
+    def __init__(self, p, A, B, order, G, H, J):
+        self.p = p
+        self.A = A
+        self.B = B
+        self.order = order
+        self.G = G
+        self.H = H
+        self.J = J
+        assert self.is_valid(G)
+        assert self.is_valid(H)
+
+    def is_valid(self, P):
+        x, y, z = P
+        if z == 0:
+            return x != 0 or y != 0
+        z_inv = ff_inv(z, self.p)
+        x, y = x * z_inv, y * z_inv
+        return y**2 % self.p == (x**3 + self.A * x + self.B) % self.p
+
+    def add(self, p1, p2):
+        x1, y1, z1 = p1
+        x2, y2, z2 = p2
+
+        if z1 == 0:
+            return (x2, y2, z2)
+        elif z2 == 0:
+            return (x1, y1, z1)
+
+        if x1 == x2:
+            if y1 != y2:
+                return (0, 1, 0)
+
+            assert y1 != 0
+            m = (3 * x1**2 + self.A) * ff_inv(2*y1, self.p)
+        else:
+            m = (y2 - y1) * ff_inv(x2 - x1, self.p)
+
+        x3 = (m**2 - x1 - x2) % self.p
+        y3 = (m * (x1 - x3) - y1) % self.p
+        return (x3, y3, 1)
+
+    def multiply(self, m, p):
+        bits = f"{m:b}"
+        result = (0, 1, 0)
+        temp = p
+        for bit in bits[::-1]:
+            if bit == "1":
+                result = self.add(result, temp)
+            temp = self.add(temp, temp)
+        return result
+
+    def random_point(self):
+        m = self.random_scalar()
+        return self.multiply(m, self.G)
+
+    def random_scalar(self):
+        m = random.randrange(0, self.order - 1)
+        return m
+
+    def random_base(self):
+        m = random.randrange(0, self.p - 1)
+        return m
+
+def pallas_curve():
+    # Pallas
+    p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001
+    q = 0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001
+    G = (5, 5392431450607408583390510508521091931943415030464003135511088002453056875732, 1)
+    H = (9762257241998025279988087154025308614062019274413483967640476725944341089207,
+         12058632856930756995627167820351407063813260358041446014729496773111030695755, 1)
+    J = (7795559447963065356059848000022900528974048197507738248625163674930282081839,
+         5156492880772775379342191094371887365795329446468828588866320184016504353483, 1)
+    ec = EllipticCurve(p, 0, 5, q, G, H, J)
+    A = (144931808354919915876542440378319484704499556634959420306426167479163065488,
+         2699682121356767698440748624399854659825391162912545787181017961871465868196, 1)
+    B = (16017037670495191561606513965775243786961447026019262496667491008912834496943,
+         20395164507282344548629891414360366999207473153143014512687861307997120664849, 1)
+    assert ec.add(A, B) == (2414658659502531855741199170408914396997834981355655923471364687102714431309, 21133344194418979683767005688724798091220515434220043854575260979109407444719, 1)
+    m = 26322809409216846271933211244226061368157231119725763192402071651286829040466
+    assert ec.multiply(m, G) == (15862887453366837597569434439063150886012590021428640083047997467990450633825, 25887284719793568129480941070850220101898092026705204234126448799557008384178, 1)
+    return ec
+
+def pedersen_encrypt(x, y, ec):
+    vcv = ec.multiply(x, ec.G)
+    vcr = ec.multiply(y, ec.H)
+    return ec.add(vcv, vcr)
+
+def _add_to_hasher(hasher, args):
+    for arg in args:
+        match arg:
+            case int() as arg:
+                hasher.update(arg.to_bytes(32, byteorder="little"))
+            case bytes() as arg:
+                hasher.update(arg)
+            case list() as arg:
+                _add_to_hasher(hasher, arg)
+            case _:
+                raise Exception(f"unknown hash arg '{arg}' type: {type(arg)}")
+
+def ff_hash(p, *args):
+    hasher = hashlib.sha256()
+    _add_to_hasher(hasher, args)
+    value = int.from_bytes(hasher.digest(), byteorder="little")
+    return value % p
+
+def hash_point(point, message=None):
+    hasher = hashlib.sha256()
+    for x_i in point:
+        hasher.update(x_i.to_bytes(32, byteorder="little"))
+    # Optional message
+    if message is not None:
+        hasher.update(message)
+    value = int.from_bytes(hasher.digest(), byteorder="little")
+    return value
+
+def sign(message, secret, ec):
+    ephem_secret = ec.random_scalar()
+    ephem_public = ec.multiply(ephem_secret, ec.G)
+    challenge = hash_point(ephem_public, message) % ec.order
+    response = (ephem_secret + challenge * secret) % ec.order
+    return ephem_public, response
+
+def verify(message, signature, public, ec):
+    ephem_public, response = signature
+    challenge = hash_point(ephem_public, message) % ec.order
+    # sG
+    lhs = ec.multiply(response, ec.G)
+    # R + cP
+    rhs_cP = ec.multiply(challenge, public)
+    rhs = ec.add(ephem_public, rhs_cP)
+    return lhs == rhs
+

+ 1463 - 0
example/dao/schema/main.py

@@ -0,0 +1,1463 @@
+import sys
+from classnamespace import ClassNamespace
+
+import crypto, money
+
+class MoneyState:
+
+    def __init__(self):
+        self.all_coins = set()
+        self.nullifiers = set()
+
+    def is_valid_merkle(self, all_coins):
+        return all_coins.issubset(self.all_coins)
+
+    def nullifier_exists(self, nullifier):
+        return nullifier in self.nullifiers
+
+    def apply(self, update):
+        self.nullifiers = self.nullifiers.union(update.nullifiers)
+
+        for coin, enc_note in zip(update.coins, update.enc_notes):
+            self.all_coins.add(coin)
+
+def money_state_transition(state, tx):
+    for input in tx.clear_inputs:
+        pk = input.signature_public
+        # Check pk is correct
+
+    for input in tx.inputs:
+        if not state.is_valid_merkle(input.revealed.all_coins):
+            print(f"invalid merkle root", file=sys.stderr)
+            return None
+
+        nullifier = input.revealed.nullifier
+        if state.nullifier_exists(nullifier):
+            print(f"duplicate nullifier found", file=sys.stderr)
+            return None
+
+    is_verify, reason = tx.verify()
+    if not is_verify:
+        print(f"tx verify failed: {reason}", file=sys.stderr)
+        return None
+
+    update = ClassNamespace()
+    update.nullifiers = [input.revealed.nullifier for input in tx.inputs]
+    update.coins = [output.revealed.coin for output in tx.outputs]
+    update.enc_notes = [output.enc_note for output in tx.outputs]
+    return update
+
+class ProposerTxBuilder:
+
+    def __init__(self, proposal, all_dao_bullas, ec):
+        self.inputs = []
+        self.proposal = proposal
+        self.all_dao_bullas = all_dao_bullas
+
+        self.ec = ec
+
+    def add_input(self, all_coins, secret, note):
+        input = ClassNamespace()
+        input.all_coins = all_coins
+        input.secret = secret
+        input.note = note
+        self.inputs.append(input)
+
+    def set_dao(self, dao):
+        self.dao = dao
+
+    def build(self):
+        tx = ProposerTx(self.ec)
+        token_blind = self.ec.random_scalar()
+        enc_bulla_blind = self.ec.random_base()
+
+        total_value = sum(input.note.value for input in self.inputs)
+        input_value_blinds = [self.ec.random_scalar() for _ in self.inputs]
+        total_value_blinds = sum(input_value_blinds)
+
+        tx.dao = ClassNamespace()
+        tx.dao.__name__ = "ProposerTxDao"
+        # We export proposer_limit as an encrypted value from the DAO
+        tx.dao.proof = ProposerTxDaoProof(
+            # Value commit
+            total_value,
+            total_value_blinds,
+            # DAO params
+            self.dao.proposer_limit,
+            self.dao.quorum,
+            self.dao.approval_ratio,
+            self.dao.gov_token_id,
+            self.dao.public_key,
+            self.dao.bulla_blind,
+            # Token commit
+            token_blind,
+            # Used by other DAO members to verify the bulla
+            # used in this proof is for the actual DAO
+            enc_bulla_blind,
+            # Proposal
+            self.proposal.dest,
+            self.proposal.amount,
+            self.proposal.serial,
+            self.proposal.token_id,
+            self.proposal.blind,
+            # Merkle witness
+            self.all_dao_bullas,
+            self.ec
+        )
+        tx.dao.revealed = tx.dao.proof.get_revealed()
+
+        # Members of the DAO need to themselves verify this is the correct
+        # bulla they are voting on, so we encrypt the blind to them
+        tx.note = ClassNamespace()
+        tx.note.enc_bulla_blind = enc_bulla_blind
+        tx.note.proposal = self.proposal
+
+        signature_secrets = []
+        for input, value_blind in zip(self.inputs, input_value_blinds):
+            signature_secret = self.ec.random_scalar()
+            signature_secrets.append(signature_secret)
+
+            tx_input = ClassNamespace()
+            tx_input.__name__ = "TransactionInput"
+            tx_input.proof = ProposerTxInputProof(
+                input.note.value, input.note.token_id, value_blind,
+                token_blind, input.note.serial, input.note.coin_blind,
+                input.secret, input.note.spend_hook, input.note.user_data,
+                input.all_coins, signature_secret, self.ec)
+            tx_input.revealed = tx_input.proof.get_revealed()
+            tx.inputs.append(tx_input)
+
+        unsigned_tx_data = tx.partial_encode()
+        for (input, signature_secret) in zip(tx.inputs, signature_secrets):
+            signature = crypto.sign(unsigned_tx_data, signature_secret, self.ec)
+            input.signature = signature
+
+        return tx
+
+class ProposerTx:
+
+    def __init__(self, ec):
+        self.inputs = []
+        self.dao = None
+        self.note = None
+
+        self.ec = ec
+
+    def partial_encode(self):
+        # There is no cake
+        return b"hello"
+
+    def verify(self):
+        if not self._check_value_commits():
+            return False, "value commits do not match"
+
+        if not self._check_proofs():
+            return False, "proofs failed to verify"
+
+        if not self._verify_token_commitments():
+            return False, "token ID mismatch"
+
+        unsigned_tx_data = self.partial_encode()
+        for input in self.inputs:
+            public = input.revealed.signature_public
+            if not crypto.verify(unsigned_tx_data, input.signature,
+                                 public, self.ec):
+                return False
+
+        return True, None
+
+    def _check_value_commits(self):
+        valcom_total = (0, 1, 0)
+
+        for input in self.inputs:
+            value_commit = input.revealed.value_commit
+            valcom_total = self.ec.add(valcom_total, value_commit)
+
+        return valcom_total == self.dao.revealed.value_commit
+
+    def _check_proofs(self):
+        for input in self.inputs:
+            if not input.proof.verify(input.revealed):
+                return False
+        if not self.dao.proof.verify(self.dao.revealed):
+            return False
+        return True
+
+    def _verify_token_commitments(self):
+        token_commit_value = self.dao.revealed.token_commit
+        for input in self.inputs:
+            if input.revealed.token_commit != token_commit_value:
+                return False
+        return True
+
+class ProposerTxInputProof:
+
+    def __init__(self, value, token_id, value_blind, token_blind, serial,
+                 coin_blind, secret, spend_hook, user_data,
+                 all_coins, signature_secret, ec):
+        self.value = value
+        self.token_id = token_id
+        self.value_blind = value_blind
+        self.token_blind = token_blind
+        self.serial = serial
+        self.coin_blind = coin_blind
+        self.secret = secret
+        self.spend_hook = spend_hook
+        self.user_data = user_data
+        self.all_coins = all_coins
+        self.signature_secret = signature_secret
+
+        self.ec = ec
+
+    def get_revealed(self):
+        revealed = ClassNamespace()
+
+        revealed.value_commit = crypto.pedersen_encrypt(
+            self.value, self.value_blind, self.ec
+        )
+        revealed.token_commit = crypto.pedersen_encrypt(
+            self.token_id, self.token_blind, self.ec
+        )
+
+        # is_valid_merkle_root()
+        revealed.all_coins = self.all_coins
+
+        revealed.signature_public = self.ec.multiply(self.signature_secret,
+                                                     self.ec.G)
+
+        return revealed
+
+    def verify(self, public):
+        revealed = self.get_revealed()
+
+        public_key = self.ec.multiply(self.secret, self.ec.G)
+        coin = crypto.ff_hash(
+            self.ec.p,
+            public_key[0],
+            public_key[1],
+            self.value,
+            self.token_id,
+            self.serial,
+            self.coin_blind,
+            self.spend_hook,
+            self.user_data,
+        )
+        # Merkle root check
+        if coin not in self.all_coins:
+            return False
+
+        return all([
+            revealed.value_commit == public.value_commit,
+            revealed.token_commit == public.token_commit,
+            revealed.all_coins == public.all_coins,
+            revealed.signature_public == public.signature_public
+        ])
+
+class ProposerTxDaoProof:
+
+    def __init__(self, total_value, total_value_blinds,
+                 proposer_limit, quorum, approval_ratio,
+                 gov_token_id, dao_public_key, dao_bulla_blind,
+                 token_blind, enc_bulla_blind,
+                 proposal_dest, proposal_amount, proposal_serial,
+                 proposal_token_id, proposal_blind,
+                 all_dao_bullas, ec):
+        self.total_value = total_value
+        self.total_value_blinds = total_value_blinds
+        self.proposer_limit = proposer_limit
+        self.quorum = quorum
+        self.approval_ratio = approval_ratio
+        self.gov_token_id = gov_token_id
+        self.dao_public_key = dao_public_key
+        self.dao_bulla_blind = dao_bulla_blind
+        self.token_blind = token_blind
+        self.enc_bulla_blind = enc_bulla_blind
+        self.proposal_dest = proposal_dest
+        self.proposal_amount = proposal_amount
+        self.proposal_serial = proposal_serial
+        self.proposal_token_id = proposal_token_id
+        self.proposal_blind = proposal_blind
+        self.all_dao_bullas = all_dao_bullas
+        self.ec = ec
+
+    def get_revealed(self):
+        revealed = ClassNamespace()
+        # Value commit
+        revealed.value_commit = crypto.pedersen_encrypt(
+            self.total_value, self.total_value_blinds, self.ec
+        )
+        # Token ID
+        revealed.token_commit = crypto.pedersen_encrypt(
+            self.gov_token_id, self.token_blind, self.ec
+        )
+        # encrypted DAO bulla
+        bulla = crypto.ff_hash(
+            self.ec.p,
+            self.proposer_limit,
+            self.quorum,
+            self.approval_ratio,
+            self.gov_token_id,
+            self.dao_public_key[0],
+            self.dao_public_key[1],
+            self.dao_bulla_blind
+        )
+        revealed.enc_bulla = crypto.ff_hash(self.ec.p, bulla, self.enc_bulla_blind)
+        # encrypted proposal
+        revealed.proposal_bulla = crypto.ff_hash(
+            self.ec.p,
+            self.proposal_dest[0],
+            self.proposal_dest[1],
+            self.proposal_amount,
+            self.proposal_serial,
+            self.proposal_token_id,
+            self.proposal_blind,
+            bulla
+        )
+        # The merkle root
+        revealed.all_dao_bullas = self.all_dao_bullas
+        return revealed
+
+    def verify(self, public):
+        revealed = self.get_revealed()
+
+        bulla = crypto.ff_hash(
+            self.ec.p,
+            self.proposer_limit,
+            self.quorum,
+            self.approval_ratio,
+            self.gov_token_id,
+            self.dao_public_key[0],
+            self.dao_public_key[1],
+            self.dao_bulla_blind
+        )
+        # Merkle root check
+        if bulla not in self.all_dao_bullas:
+            return False
+
+        # This should not be able to be bigger than 2^64
+        assert self.proposal_amount > 0
+
+        #
+        #   total_value >= proposer_limit
+        #
+        if not self.total_value >= self.proposer_limit:
+            return False
+
+        return all([
+            revealed.value_commit == public.value_commit,
+            revealed.token_commit == public.token_commit,
+            revealed.enc_bulla == public.enc_bulla,
+            revealed.proposal_bulla == public.proposal_bulla,
+            revealed.all_dao_bullas == public.all_dao_bullas
+        ])
+
+class VoteTxBuilder:
+
+    def __init__(self, ec):
+        self.inputs = []
+        self.vote_option = None
+
+        self.ec = ec
+
+    def add_input(self, all_coins, secret, note):
+        input = ClassNamespace()
+        input.all_coins = all_coins
+        input.secret = secret
+        input.note = note
+        self.inputs.append(input)
+
+    def set_vote_option(self, vote_option):
+        assert vote_option == 0 or vote_option == 1
+        self.vote_option = vote_option
+
+    def build(self):
+        tx = VoteTx(self.ec)
+        token_blind = self.ec.random_scalar()
+
+        assert self.vote_option is not None
+        vote_option_blind = self.ec.random_base()
+
+        total_value, total_blind = 0, 0
+        signature_secrets = []
+        for input in self.inputs:
+            value_blind = self.ec.random_scalar()
+            total_blind = (total_blind + value_blind) % self.ec.order
+            total_value = (total_value + input.note.value) % self.ec.order
+
+            signature_secret = self.ec.random_scalar()
+            signature_secrets.append(signature_secret)
+
+            tx_input = ClassNamespace()
+            tx_input.__name__ = "TransactionInput"
+            tx_input.burn_proof = VoteBurnProof(
+                input.note.value, input.note.token_id, value_blind,
+                token_blind, input.note.serial, input.note.coin_blind,
+                input.secret, input.note.spend_hook, input.note.user_data,
+                input.all_coins, signature_secret,
+                self.ec)
+            tx_input.revealed = tx_input.burn_proof.get_revealed()
+            tx.inputs.append(tx_input)
+
+        assert len(self.inputs) > 0
+        token_id = self.inputs[0].note.token_id
+
+        vote_blind = self.ec.random_scalar()
+
+        # This whole tx is like just burning tokens
+        # except we produce an output commitment to the total value in
+        tx.vote = ClassNamespace()
+        tx.vote.__name__ = "Vote"
+        tx.vote.proof = VoteProof(total_value, token_id,
+                                  total_blind, token_blind, vote_blind,
+                                  self.vote_option, vote_option_blind,
+                                  self.ec)
+        tx.vote.revealed = tx.vote.proof.get_revealed()
+
+        # We can use Shamir's Secret Sharing to unlock this at the end
+        # of the voting, or even with a time delay to avoid timing attacks
+        tx.note = ClassNamespace()
+        tx.note.__name__ = "EncryptedNoteForDaoMembers"
+        tx.note.value = total_value
+        tx.note.token_id = token_id
+        tx.note.vote_option = self.vote_option
+        tx.note.value_blind = total_blind
+        tx.note.token_blind = token_blind
+        tx.note.vote_blind = vote_blind
+        tx.note.vote_option_blind = vote_option_blind
+
+        unsigned_tx_data = tx.partial_encode()
+        for (input, signature_secret) in zip(tx.inputs, signature_secrets):
+            signature = crypto.sign(unsigned_tx_data, signature_secret, self.ec)
+            input.signature = signature
+
+        return tx
+
+class VoteBurnProof:
+
+    def __init__(self, value, token_id,
+                 value_blind, token_blind, serial,
+                 coin_blind, secret, spend_hook, user_data,
+                 all_coins, signature_secret, ec):
+        self.value = value
+        self.token_id = token_id
+        self.value_blind = value_blind
+        self.token_blind = token_blind
+        self.serial = serial
+        self.coin_blind = coin_blind
+        self.secret = secret
+        self.spend_hook = spend_hook
+        self.user_data = user_data
+        self.all_coins = all_coins
+        self.signature_secret = signature_secret
+
+        self.ec = ec
+
+    def get_revealed(self):
+        revealed = ClassNamespace()
+        revealed.nullifier = crypto.ff_hash(self.ec.p, self.secret, self.serial)
+
+        revealed.value_commit = crypto.pedersen_encrypt(
+            self.value, self.value_blind, self.ec
+        )
+
+        revealed.token_commit = crypto.pedersen_encrypt(
+            self.token_id, self.token_blind, self.ec
+        )
+
+        # is_valid_merkle_root()
+        revealed.all_coins = self.all_coins
+
+        revealed.signature_public = self.ec.multiply(self.signature_secret,
+                                                     self.ec.G)
+
+        return revealed
+
+    def verify(self, public):
+        revealed = self.get_revealed()
+
+        public_key = self.ec.multiply(self.secret, self.ec.G)
+        coin = crypto.ff_hash(
+            self.ec.p,
+            public_key[0],
+            public_key[1],
+            self.value,
+            self.token_id,
+            self.serial,
+            self.coin_blind,
+            self.spend_hook,
+            self.user_data,
+        )
+        # Merkle root check
+        if coin not in self.all_coins:
+            return False
+
+        return all([
+            revealed.nullifier == public.nullifier,
+            revealed.value_commit == public.value_commit,
+            revealed.token_commit == public.token_commit,
+            revealed.all_coins == public.all_coins,
+            revealed.signature_public == public.signature_public,
+        ])
+
+class VoteProof:
+
+    def __init__(self, value, token_id,
+                 value_blind, token_blind, vote_blind,
+                 vote_option, vote_option_blind, ec):
+        self.value = value
+        self.token_id = token_id
+        self.value_blind = value_blind
+        self.token_blind = token_blind
+        self.vote_blind = vote_blind
+        self.vote_option = vote_option
+        self.vote_option_blind = vote_option_blind
+        self.ec = ec
+
+    def get_revealed(self):
+        revealed = ClassNamespace()
+        # Multiply the point by vote_option
+        revealed.value_commit = crypto.pedersen_encrypt(
+            self.value, self.value_blind, self.ec
+        )
+        revealed.vote_commit = crypto.pedersen_encrypt(
+            self.vote_option * self.value, self.vote_blind, self.ec
+        )
+        revealed.token_commit = crypto.pedersen_encrypt(
+            self.token_id, self.token_blind, self.ec
+        )
+        #revealed.vote_option_commit = crypto.ff_hash(
+        #    self.ec.p, self.vote_option, self.vote_option_blind
+        #)
+        return revealed
+
+    def verify(self, public):
+        revealed = self.get_revealed()
+        # vote option should be 0 or 1
+        if ((self.vote_option - 0) * (self.vote_option - 1)) % self.ec.p != 0:
+            return False
+        return all([
+            revealed.value_commit == public.value_commit,
+            revealed.vote_commit == public.vote_commit,
+            revealed.token_commit == public.token_commit,
+            #revealed.vote_option_commit == public.vote_option_commit
+        ])
+
+class VoteTx:
+
+    def __init__(self, ec):
+        self.inputs = []
+        self.vote = None
+
+        self.ec = ec
+
+    def partial_encode(self):
+        # There is no cake
+        return b"hello"
+
+    def verify(self):
+        if not self._check_value_commits():
+            return False, "value commits do not match"
+
+        if not self._check_proofs():
+            return False, "proofs failed to verify"
+
+        if not self._verify_token_commitments():
+            return False, "token ID mismatch"
+
+        return True, None
+
+    def _check_value_commits(self):
+        valcom_total = (0, 1, 0)
+        for input in self.inputs:
+            value_commit = input.revealed.value_commit
+            valcom_total = self.ec.add(valcom_total, value_commit)
+
+        return valcom_total == self.vote.revealed.value_commit
+
+    def _check_proofs(self):
+        for input in self.inputs:
+            if not input.burn_proof.verify(input.revealed):
+                return False
+        if not self.vote.proof.verify(self.vote.revealed):
+            return False
+        return True
+
+    def _verify_token_commitments(self):
+        token_commit_value = self.vote.revealed.token_commit
+        for input in self.inputs:
+            if input.revealed.token_commit != token_commit_value:
+                return False
+        return True
+
+class DaoBuilder:
+
+    def __init__(self, proposer_limit, quorum, approval_ratio,
+                 gov_token_id, dao_public_key, dao_bulla_blind, ec):
+        self.proposer_limit = proposer_limit
+        self.quorum = quorum
+        self.approval_ratio = approval_ratio
+        self.gov_token_id = gov_token_id
+        self.dao_public_key = dao_public_key
+        self.dao_bulla_blind = dao_bulla_blind
+
+        self.ec = ec
+
+    def build(self):
+        mint_proof = DaoMintProof(
+            self.proposer_limit,
+            self.quorum,
+            self.approval_ratio,
+            self.gov_token_id,
+            self.dao_public_key,
+            self.dao_bulla_blind,
+            self.ec
+        )
+        revealed = mint_proof.get_revealed()
+
+        dao = Dao(revealed, mint_proof, self.ec)
+        return dao
+
+class Dao:
+
+    def __init__(self, revealed, mint_proof, ec):
+        self.revealed = revealed
+        self.mint_proof = mint_proof
+        self.ec = ec
+
+    def verify(self):
+        if not self.mint_proof.verify(self.revealed):
+            return False, "mint proof failed to verify"
+        return True, None
+
+# class DaoExec .etc
+
+class DaoMintProof:
+
+    def __init__(self, proposer_limit, quorum, approval_ratio,
+                 gov_token_id, dao_public_key, dao_bulla_blind, ec):
+        self.proposer_limit = proposer_limit
+        self.quorum = quorum
+        self.approval_ratio = approval_ratio
+        self.gov_token_id = gov_token_id
+        self.dao_public_key = dao_public_key
+        self.dao_bulla_blind = dao_bulla_blind
+        self.ec = ec
+
+    def get_revealed(self):
+        revealed = ClassNamespace()
+
+        revealed.bulla = crypto.ff_hash(
+            self.ec.p,
+            self.proposer_limit,
+            self.quorum,
+            self.approval_ratio,
+            self.gov_token_id,
+            self.dao_public_key[0],
+            self.dao_public_key[1],
+            self.dao_bulla_blind
+        )
+
+        return revealed
+
+    def verify(self, public):
+        revealed = self.get_revealed()
+        return revealed.bulla == public.bulla
+
+# Shared between DaoMint and DaoExec
+class DaoState:
+
+    def __init__(self):
+        self.dao_bullas = set()
+        self.proposals = set()
+        # Closed proposals
+        self.proposal_nullifiers = set()
+
+    def is_valid_merkle(self, all_dao_bullas):
+        return all_dao_bullas.issubset(self.dao_bullas)
+
+    def is_valid_merkle_proposals(self, all_proposal_bullas):
+        return all_proposal_bullas.issubset(self.proposals)
+
+    def proposal_nullifier_exists(self, nullifier):
+        return nullifier in self.proposal_nullifiers
+
+    def apply_proposal_tx(self, update):
+        self.proposals.add(update.proposal)
+
+    def apply_exec_tx(self, update):
+        self.proposal_nullifiers.add(update.proposal_nullifier)
+
+    # Apply DAO mint tx update
+    def apply(self, update):
+        self.dao_bullas.add(update.bulla)
+
+# contract interface functions
+def dao_state_transition(state, tx):
+    is_verify, reason = tx.verify()
+    if not is_verify:
+        print(f"dao tx verify failed: {reason}", file=sys.stderr)
+        return None
+
+    update = ClassNamespace()
+    update.bulla = tx.revealed.bulla
+    return update
+
+###### DAO EXEC
+
+class DaoExecBuilder:
+
+    def __init__(self,
+        proposal,
+        all_proposals,
+        dao,
+        win_votes,
+        total_votes,
+        total_value_blinds,
+        total_vote_blinds,
+        pay_tx_serial_0,
+        pay_tx_serial_1,
+        pay_tx_coin_blind_0,
+        pay_tx_coin_blind_1,
+        pay_tx_input_value,
+        pay_tx_input_blinds,
+        ec
+    ):
+        self.proposal = proposal
+        self.all_proposals = all_proposals
+        self.dao = dao
+        self.win_votes = win_votes
+        self.total_votes = total_votes
+        self.total_value_blinds = total_value_blinds
+        self.total_vote_blinds = total_vote_blinds
+        self.pay_tx_serial_0 = pay_tx_serial_0
+        self.pay_tx_serial_1 = pay_tx_serial_1
+        self.pay_tx_coin_blind_0 = pay_tx_coin_blind_0
+        self.pay_tx_coin_blind_1 = pay_tx_coin_blind_1
+        self.pay_tx_input_value = pay_tx_input_value
+        self.pay_tx_input_blinds = pay_tx_input_blinds
+
+        self.ec = ec
+
+    def build(self):
+        tx = DaoExecTx()
+        tx.proof = DaoExecProof(
+            self.proposal,
+            self.all_proposals,
+            self.dao,
+            self.win_votes,
+            self.total_votes,
+            self.total_value_blinds,
+            self.total_vote_blinds,
+            self.pay_tx_serial_0,
+            self.pay_tx_serial_1,
+            self.pay_tx_coin_blind_0,
+            self.pay_tx_coin_blind_1,
+            self.pay_tx_input_value,
+            self.pay_tx_input_blinds,
+            self.ec
+        )
+        tx.revealed = tx.proof.get_revealed()
+        return tx
+
+class DaoExecTx:
+
+    def verify(self):
+        if not self._check_proofs():
+            return False, "proofs failed to verify"
+
+        return True, None
+
+    def _check_proofs(self):
+        if not self.proof.verify(self.revealed):
+            return False
+        return True
+
+class DaoExecProof:
+
+    def __init__(self,
+        proposal,
+        all_proposals,
+        dao,
+        win_votes,
+        total_votes,
+        total_value_blinds,
+        total_vote_blinds,
+        pay_tx_serial_0,
+        pay_tx_serial_1,
+        pay_tx_coin_blind_0,
+        pay_tx_coin_blind_1,
+        pay_tx_input_value,
+        pay_tx_input_blinds,
+        ec
+    ):
+        self.proposal = proposal
+        self.all_proposals = all_proposals
+        self.dao = dao
+        self.win_votes = win_votes
+        self.total_votes = total_votes
+        self.total_value_blinds = total_value_blinds
+        self.total_vote_blinds = total_vote_blinds
+        self.pay_tx_serial_0 = pay_tx_serial_0
+        self.pay_tx_serial_1 = pay_tx_serial_1
+        self.pay_tx_coin_blind_0 = pay_tx_coin_blind_0
+        self.pay_tx_coin_blind_1 = pay_tx_coin_blind_1
+        self.pay_tx_input_value = pay_tx_input_value
+        self.pay_tx_input_blinds = pay_tx_input_blinds
+
+        self.ec = ec
+
+    def get_revealed(self):
+        revealed = ClassNamespace()
+        # Corresponds to proposals merkle root
+        revealed.all_proposals = self.all_proposals
+
+        dao_bulla = crypto.ff_hash(
+            self.ec.p,
+            self.dao.proposer_limit,
+            self.dao.quorum,
+            self.dao.approval_ratio,
+            self.dao.gov_token_id,
+            self.dao.public_key[0],
+            self.dao.public_key[1],
+            self.dao.bulla_blind
+        )
+        proposal_bulla = crypto.ff_hash(
+            self.ec.p,
+            self.proposal.dest[0],
+            self.proposal.dest[1],
+            self.proposal.amount,
+            self.proposal.serial,
+            self.proposal.token_id,
+            self.proposal.blind,
+            dao_bulla
+        )
+        revealed.proposal_nullifier = crypto.ff_hash(
+            self.ec.p, self.proposal.serial)
+
+        revealed.coin_0 = crypto.ff_hash(
+            self.ec.p,
+            self.proposal.dest[0],
+            self.proposal.dest[1],
+            self.proposal.amount,
+            self.proposal.token_id,
+            self.pay_tx_serial_0,
+            self.pay_tx_coin_blind_0,
+            b"0x0000",
+            b"0x0000"
+        )
+
+        change_amount = self.pay_tx_input_value - self.proposal.amount
+        assert change_amount > 0
+
+        # Need the same DAO public key
+        # Need the input amount for pay_tx for treasury
+        # Need user_data blind
+        revealed.coin_1 = crypto.ff_hash(
+            self.ec.p,
+            self.dao.public_key[0],
+            self.dao.public_key[1],
+            change_amount,
+            self.proposal.token_id,
+            self.pay_tx_serial_1,
+            self.pay_tx_coin_blind_1,
+            b"0xdao_ruleset",
+            dao_bulla
+        )
+
+        # Money that went into the pay tx
+        revealed.inputs_value_commit = crypto.pedersen_encrypt(
+            self.pay_tx_input_value, self.pay_tx_input_blinds, self.ec)
+
+        revealed.total_value_commit = crypto.pedersen_encrypt(
+            self.total_votes, self.total_value_blinds, self.ec)
+        revealed.total_vote_commit = crypto.pedersen_encrypt(
+            self.win_votes, self.total_vote_blinds, self.ec)
+
+        return revealed
+
+    def verify(self, public):
+        revealed = self.get_revealed()
+
+        # Check proposal exists
+        dao_bulla = crypto.ff_hash(
+            self.ec.p,
+            self.dao.proposer_limit,
+            self.dao.quorum,
+            self.dao.approval_ratio,
+            self.dao.gov_token_id,
+            self.dao.public_key[0],
+            self.dao.public_key[1],
+            self.dao.bulla_blind
+        )
+        proposal_bulla = crypto.ff_hash(
+            self.ec.p,
+            self.proposal.dest[0],
+            self.proposal.dest[1],
+            self.proposal.amount,
+            self.proposal.serial,
+            self.proposal.token_id,
+            self.proposal.blind,
+            dao_bulla
+        )
+        # This being true also implies the DAO is valid
+        assert proposal_bulla in self.all_proposals
+
+        assert self.total_votes >= self.dao.quorum
+
+        # Approval ratio should be actually 2 values ffs
+        #assert self.win_votes / self.total_votes >= self.dao.approval_ratio
+        assert self.win_votes >= self.dao.approval_ratio * self.total_votes
+
+        return all([
+            revealed.all_proposals == public.all_proposals,
+            revealed.proposal_nullifier == public.proposal_nullifier,
+            revealed.coin_0 == public.coin_0,
+            revealed.coin_1 == public.coin_1,
+            revealed.inputs_value_commit == public.inputs_value_commit,
+            revealed.total_value_commit == public.total_value_commit,
+            revealed.total_vote_commit == public.total_vote_commit,
+        ])
+
+def dao_exec_state_transition(state, tx, pay_tx, ec):
+    is_verify, reason = tx.verify()
+    if not is_verify:
+        print(f"dao exec tx verify failed: {reason}", file=sys.stderr)
+        return None
+
+    if not state.is_valid_merkle_proposals(tx.revealed.all_proposals):
+        print(f"invalid merkle root proposals", file=sys.stderr)
+        return None
+
+    nullifier = tx.revealed.proposal_nullifier
+    if state.proposal_nullifier_exists(nullifier):
+        print(f"duplicate nullifier found", file=sys.stderr)
+        return None
+
+    # Check the structure of the payment tx is correct
+    if len(pay_tx.outputs) != 2:
+        print(f"only 2 outputs allowed", file=sys.stderr)
+        return None
+    if tx.revealed.coin_0 != pay_tx.outputs[0].revealed.coin:
+        print(f"coin0 incorrectly formed", file=sys.stderr)
+        return None
+
+    inputs_value_commit = (0, 1, 0)
+    for input in pay_tx.inputs:
+        value_commit = input.revealed.value_commit
+        inputs_value_commit = ec.add(inputs_value_commit, value_commit)
+    if inputs_value_commit != tx.revealed.inputs_value_commit:
+        print(f"value commitment for inputs doesn't match", file=sys.stderr)
+        return None
+
+    if tx.revealed.coin_1 != pay_tx.outputs[1].revealed.coin:
+        print(f"coin1 incorrectly formed", file=sys.stderr)
+        return None
+
+    update = ClassNamespace()
+    update.proposal_nullifier = tx.revealed.proposal_nullifier
+    return update
+
+# contract interface functions
+def proposal_state_transition(dao_state, gov_state, tx):
+    is_verify, reason = tx.verify()
+    if not is_verify:
+        print(f"dao tx verify failed: {reason}", file=sys.stderr)
+        return None
+
+    if not dao_state.is_valid_merkle(tx.dao.revealed.all_dao_bullas):
+        print(f"invalid merkle root dao", file=sys.stderr)
+        return None
+
+    for input in tx.inputs:
+        if not gov_state.is_valid_merkle(input.revealed.all_coins):
+            print(f"invalid merkle root", file=sys.stderr)
+            return None
+
+    update = ClassNamespace()
+    update.proposal = tx.dao.revealed.proposal_bulla
+    return update
+
+class VoteState:
+
+    def __init__(self):
+        self.votes = set()
+        self.nullifiers = set()
+
+    def nullifier_exists(self, nullifier):
+        return nullifier in self.nullifiers
+
+    def apply(self, update):
+        self.nullifiers = self.nullifiers.union(update.nullifiers)
+        self.votes.add(update.vote)
+
+def vote_state_transition(vote_state, gov_state, tx):
+    for input in tx.inputs:
+        if not gov_state.is_valid_merkle(input.revealed.all_coins):
+            print(f"invalid merkle root", file=sys.stderr)
+            return None
+
+        nullifier = input.revealed.nullifier
+        if gov_state.nullifier_exists(nullifier):
+            print(f"duplicate nullifier found", file=sys.stderr)
+            return None
+
+        if vote_state.nullifier_exists(nullifier):
+            print(f"duplicate nullifier found (already voted)", file=sys.stderr)
+            return None
+
+    is_verify, reason = tx.verify()
+    if not is_verify:
+        print(f"dao tx verify failed: {reason}", file=sys.stderr)
+        return None
+
+    update = ClassNamespace()
+    update.nullifiers = [input.revealed.nullifier for input in tx.inputs]
+    update.vote = tx.vote.revealed.value_commit
+    return update
+
+def main(argv):
+    ec = crypto.pallas_curve()
+
+    money_state = MoneyState()
+    gov_state = MoneyState()
+    dao_state = DaoState()
+
+    # Money parameters
+    money_initial_supply = 21000
+    money_token_id = 110
+
+    # Governance token parameters
+    gov_initial_supply = 10000
+    gov_token_id = 4
+
+    # DAO parameters
+    dao_proposer_limit = 110
+    dao_quorum = 110
+    dao_approval_ratio = 2
+
+    ################################################
+    # Create the DAO bulla
+    ################################################
+    # Setup the DAO
+    dao_shared_secret = ec.random_scalar()
+    dao_public_key = ec.multiply(dao_shared_secret, ec.G)
+
+    dao_bulla_blind = ec.random_base()
+
+    builder = DaoBuilder(
+        dao_proposer_limit,
+        dao_quorum,
+        dao_approval_ratio,
+        gov_token_id,
+        dao_public_key,
+        dao_bulla_blind,
+        ec
+    )
+    tx = builder.build()
+
+    # Each deployment of a contract has a unique state
+    # associated with it.
+    if (update := dao_state_transition(dao_state, tx)) is None:
+        return -1
+    dao_state.apply(update)
+
+    dao_bulla = tx.revealed.bulla
+
+    ################################################
+    # Mint the initial supply of treasury token
+    # and send it all to the DAO directly
+    ################################################
+
+    # Only used for this tx. Discarded after
+    signature_secret = ec.random_scalar()
+
+    builder = money.SendPaymentTxBuilder(ec)
+    builder.add_clear_input(money_initial_supply, money_token_id,
+                            signature_secret)
+    # Address of deployed contract in our example is 0xdao_ruleset
+    # This field is public, you can see it's being sent to a DAO
+    # but nothing else is visible.
+    spend_hook = b"0xdao_ruleset"
+    # This can be a simple hash of the items passed into the ZK proof
+    # up to corresponding linked ZK proof to interpret however they need.
+    # In out case, it's the bulla for the DAO
+    user_data = dao_bulla
+    builder.add_output(money_initial_supply, money_token_id, dao_public_key,
+                       spend_hook, user_data)
+    tx = builder.build()
+
+    # This state_transition function is the ruleset for anon payments
+    if (update := money_state_transition(money_state, tx)) is None:
+        return -1
+    money_state.apply(update)
+
+    # NOTE: maybe we want to add additional zk proof here that the tx
+    #       sending money to the DAO was constructed correctly.
+    #       For example that the user_data is set correctly
+
+    # payment state transition in coin specifies dependency
+    # the tx exists and ruleset is applied
+
+    assert len(tx.outputs) > 0
+    coin_note = tx.outputs[0].enc_note
+    coin = crypto.ff_hash(
+        ec.p,
+        dao_public_key[0],
+        dao_public_key[1],
+        coin_note.value,
+        coin_note.token_id,
+        coin_note.serial,
+        coin_note.coin_blind,
+        spend_hook,
+        user_data
+    )
+    assert coin == tx.outputs[0].mint_proof.get_revealed().coin
+
+    for coin, enc_note in zip(update.coins, update.enc_notes):
+        # Try decrypt note here
+        print(f"Received {enc_note.value} DRK")
+
+    ################################################
+    # Mint the governance token
+    # Send it to three hodlers
+    ################################################
+
+    # Hodler 1
+    gov_secret_1 = ec.random_scalar()
+    gov_public_1 = ec.multiply(gov_secret_1, ec.G)
+    # Hodler 2
+    gov_secret_2 = ec.random_scalar()
+    gov_public_2 = ec.multiply(gov_secret_2, ec.G)
+    # Hodler 3: the tiebreaker
+    gov_secret_3 = ec.random_scalar()
+    gov_public_3 = ec.multiply(gov_secret_3, ec.G)
+
+    # Only used for this tx. Discarded after
+    signature_secret = ec.random_scalar()
+
+    builder = money.SendPaymentTxBuilder(ec)
+    builder.add_clear_input(gov_initial_supply, gov_token_id,
+                            signature_secret)
+    assert 2 * 4000 + 2000 == gov_initial_supply
+    builder.add_output(4000, gov_token_id, gov_public_1,
+                       b"0x0000", b"0x0000")
+    builder.add_output(4000, gov_token_id, gov_public_2,
+                       b"0x0000", b"0x0000")
+    builder.add_output(2000, gov_token_id, gov_public_3,
+                       b"0x0000", b"0x0000")
+    tx = builder.build()
+
+    # This state_transition function is the ruleset for anon payments
+    if (update := money_state_transition(gov_state, tx)) is None:
+        return -1
+    gov_state.apply(update)
+
+    # Decrypt output notes
+    assert len(tx.outputs) == 3
+    gov_user_1_note = tx.outputs[0].enc_note
+    gov_user_2_note = tx.outputs[1].enc_note
+    gov_user_3_note = tx.outputs[2].enc_note
+
+    for coin, enc_note in zip(update.coins, update.enc_notes):
+        # Try decrypt note here
+        print(f"Received {enc_note.value} GOV")
+
+    ################################################
+    # DAO rules:
+    # 1. gov token IDs must match on all inputs
+    # 2. proposals must be submitted by minimum amount
+    #       - need protection so can't collude? must be a single signer??
+    #         - stellar: doesn't have to be robust for this MVP
+    # 3. number of votes >= quorum
+    #       - just positive votes or all votes?
+    #         - stellar: no that's all votes
+    # 4. outcome > approval_ratio
+    # 5. structure of outputs
+    #   output 0: value and address
+    #   output 1: change address
+    ################################################
+
+    ################################################
+    # Propose the vote
+    # In order to make a valid vote, first the proposer must
+    # meet a criteria for a minimum number of gov tokens
+    ################################################
+
+    user_secret = ec.random_scalar()
+    user_public = ec.multiply(user_secret, ec.G)
+
+    # There is a struct that corresponds to the configuration of this
+    # particular vote.
+    # For MVP, just use a single-option list of [destination, amount]
+    # Send user 1000 DRK
+    proposal = ClassNamespace()
+    proposal.dest = user_public
+    proposal.amount = 1000
+    # Used to produce the nullifier when the vote is executed
+    proposal.serial = ec.random_base()
+    proposal.token_id = money_token_id
+    proposal.blind = ec.random_base()
+
+    # For vote to become valid, the proposer must prove
+    # that they own more than proposer_limit number of gov tokens.
+
+    dao = ClassNamespace()
+    dao.proposer_limit = dao_proposer_limit
+    dao.quorum = dao_quorum
+    dao.approval_ratio = dao_approval_ratio
+    dao.gov_token_id = gov_token_id
+    dao.public_key = dao_public_key
+    dao.bulla_blind = dao_bulla_blind
+
+    builder = ProposerTxBuilder(proposal, dao_state.dao_bullas, ec)
+    witness = gov_state.all_coins
+    builder.add_input(witness, gov_secret_1, gov_user_1_note)
+    builder.set_dao(dao)
+    tx = builder.build()
+
+    # No state changes actually happen so ignore the update
+    # We just verify the tx is correct basically.
+    if (update := proposal_state_transition(dao_state, gov_state, tx)) is None:
+        return -1
+    dao_state.apply_proposal_tx(update)
+
+    ################################################
+    # Proposal is accepted!
+    # Start the voting
+    ################################################
+
+    # Lets the voting begin
+    # Voters have access to the proposal and dao data
+    vote_state = VoteState()
+    # We don't need to copy nullifier set because it is checked from gov_state
+    # in vote_state_transition() anyway
+
+    # TODO: what happens if voters don't unblind their vote
+    # Answer:
+    #   1. there is a time limit
+    #   2. both the MPC or users can unblind
+
+    # TODO: bug if I vote then send money, then we can double vote
+    # TODO: all timestamps missing
+    #       - timelock (future voting starts in 2 days)
+    # Fix: use nullifiers from money gov state only from
+    # beginning of gov period
+    # Cannot use nullifiers from before voting period
+
+    # User 1: YES
+    builder = VoteTxBuilder(ec)
+    builder.add_input(witness, gov_secret_1, gov_user_1_note)
+    builder.set_vote_option(1)
+    tx1 = builder.build()
+
+    if (update := vote_state_transition(vote_state, gov_state, tx1)) is None:
+        return -1
+    vote_state.apply(update)
+
+    note_vote_1 = tx1.note
+
+    # User 2: NO
+    builder = VoteTxBuilder(ec)
+    builder.add_input(witness, gov_secret_2, gov_user_2_note)
+    builder.set_vote_option(0)
+    tx2 = builder.build()
+
+    if (update := vote_state_transition(vote_state, gov_state, tx2)) is None:
+        return -1
+    vote_state.apply(update)
+
+    note_vote_2 = tx2.note
+
+    # User 3: YES
+    builder = VoteTxBuilder(ec)
+    builder.add_input(witness, gov_secret_3, gov_user_3_note)
+    builder.set_vote_option(1)
+    tx3 = builder.build()
+
+    if (update := vote_state_transition(vote_state, gov_state, tx3)) is None:
+        return -1
+    vote_state.apply(update)
+
+    note_vote_3 = tx3.note
+
+    # State
+    # functions that can be called on state with params
+    # functions return an update
+    # optional encrypted values that can be read by wallets
+    # --> (do this outside??)
+    # --> penalized if fail
+    # apply update to state
+
+    # Every votes produces a semi-homomorphic encryption of their vote.
+    # Which is either yes or no
+    # We copy the state tree for the governance token so coins can be used
+    # to vote on other proposals at the same time.
+    # With their vote, they produce a ZK proof + nullifier
+    # The votes are unblinded by MPC to a selected party at the end of the
+    # voting period.
+    # (that's if we want votes to be hidden during voting)
+
+    win_votes = 0
+    total_votes = 0
+    total_vote_blinds = 0
+    total_value_blinds = 0
+    total_value_commit = (0, 1, 0)
+    total_vote_commit = (0, 1, 0)
+    for i, (note, tx) in enumerate(
+        zip([note_vote_1, note_vote_2, note_vote_3], [tx1, tx2, tx3])):
+
+        assert note.token_id == gov_token_id
+        token_commit = crypto.pedersen_encrypt(
+            gov_token_id, note.token_blind, ec)
+        assert tx.vote.revealed.token_commit == token_commit
+
+        #vote_option_commit = crypto.ff_hash(
+        #    ec.p, note.vote_option, note.vote_option_blind)
+        #assert tx.vote.revealed.vote_option_commit == vote_option_commit
+
+        value_commit = crypto.pedersen_encrypt(
+            note.value, note.value_blind, ec)
+        assert tx.vote.revealed.value_commit == value_commit
+        total_value_commit = ec.add(total_value_commit, value_commit)
+        total_value_blinds += note.value_blind
+        
+        vote_commit = crypto.pedersen_encrypt(
+            note.vote_option * note.value, note.vote_blind, ec)
+        assert tx.vote.revealed.vote_commit == vote_commit
+        total_vote_commit = ec.add(total_vote_commit, vote_commit)
+        total_vote_blinds += note.vote_blind
+
+        vote_option = note.vote_option
+        assert vote_option == 0 or vote_option == 1
+
+        if vote_option == 1:
+            win_votes += note.value
+
+        total_votes += note.value
+
+        if vote_option == 1:
+            vote_result = "yes"
+        else:
+            vote_result = "no"
+        print(f"Voter {i} voted {vote_result}")
+
+    print(f"Outcome = {win_votes} / {total_votes}")
+
+    assert total_value_commit == crypto.pedersen_encrypt(
+        total_votes, total_value_blinds, ec)
+    assert total_vote_commit == crypto.pedersen_encrypt(
+        win_votes, total_vote_blinds, ec)
+
+    ################################################
+    # Execute the vote
+    ################################################
+
+    # Used to export user_data from this coin so it can be accessed
+    # by 0xdao_ruleset
+    user_data_blind = ec.random_base()
+
+    builder = money.SendPaymentTxBuilder(ec)
+    witness = money_state.all_coins
+    builder.add_input(witness, dao_shared_secret, coin_note, user_data_blind)
+
+    builder.add_output(1000, money_token_id, user_public,
+                       spend_hook=b"0x0000", user_data=b"0x0000")
+    # Change
+    builder.add_output(coin_note.value - 1000, money_token_id, dao_public_key,
+                       spend_hook, user_data)
+
+    tx = builder.build()
+
+    if (update := money_state_transition(money_state, tx)) is None:
+        return -1
+    money_state.apply(update)
+
+    # Now the spend_hook field specifies the function DaoExec
+    # so the tx above must also be combined with a DaoExec tx
+    assert len(tx.inputs) == 1
+    # At least one input has this field value which means the 0xdao_ruleset
+    # is invoked.
+    input = tx.inputs[0]
+    assert input.revealed.spend_hook == b"0xdao_ruleset"
+    assert (input.revealed.enc_user_data ==
+        crypto.ff_hash(
+            ec.p,
+            user_data,
+            user_data_blind
+        ))
+    # Verifier cannot see DAO bulla
+    # They see the enc_user_data which is also in the DAO exec contract
+    assert user_data == crypto.ff_hash(
+        ec.p,
+        dao_proposer_limit,
+        dao_quorum,
+        dao_approval_ratio,
+        gov_token_id,
+        dao_public_key[0],
+        dao_public_key[1],
+        dao_bulla_blind
+    ) # DAO bulla
+
+    pay_tx = tx
+
+    # execution proof
+    # 1. total votes >= quorum
+    # 2. win_votes / total_votes >= approval_ratio
+    # 3. structure of outputs
+    #   output 0: value and address
+    #   output 1: change address
+
+    # - check proposal exists
+    # - create proposal nullifier
+    #     - verifier: check it doesn't already exist
+    # - check dest, amount, token_id match
+    #     - export both output value_commits
+    #     - export token_id commit used in send_payment tx
+    #     - export output 0 and 1 dest
+    #     - check all these fields match the tx
+    # - is linked to DAO
+    #     - read DAO params
+    #     - re-export as enc_user_data
+    #     - verifier: check it matches the tx
+    # - total_votes >= quorum
+    #     - verifier: check sum of vote_commits is correct
+    # - win_votes / total_votes >= approval_ratio
+
+    assert len(pay_tx.outputs) == 2
+    pay_tx_serial_0 = pay_tx.outputs[0].enc_note.serial
+    pay_tx_serial_1 = pay_tx.outputs[1].enc_note.serial
+    pay_tx_coin_blind_0 = pay_tx.outputs[0].enc_note.coin_blind
+    pay_tx_coin_blind_1 = pay_tx.outputs[1].enc_note.coin_blind
+    pay_tx_input_value = coin_note.value
+    pay_tx_input_blinds = sum(builder.input_blinds) % ec.order
+
+    builder = DaoExecBuilder(
+        proposal,
+        dao_state.proposals,
+        dao,
+        win_votes,
+        total_votes,
+        total_value_blinds,
+        total_vote_blinds,
+        pay_tx_serial_0,
+        pay_tx_serial_1,
+        pay_tx_coin_blind_0,
+        pay_tx_coin_blind_1,
+        pay_tx_input_value,
+        pay_tx_input_blinds,
+        ec
+    )
+    tx = builder.build()
+    if (update := dao_exec_state_transition(dao_state, tx, pay_tx, ec)) is None:
+        return -1
+    dao_state.apply_exec_tx(update)
+
+    # These checks are also run by the verifier
+    assert tx.revealed.total_value_commit == total_value_commit
+    assert tx.revealed.total_vote_commit == total_vote_commit
+
+    return 0
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
+

+ 331 - 0
example/dao/schema/money.py

@@ -0,0 +1,331 @@
+from classnamespace import ClassNamespace
+from crypto import ff_hash, pedersen_encrypt, sign, verify
+
+# Tx representing send_payment() contract call
+class SendPaymentTxBuilder:
+
+    def __init__(self, ec):
+        self.clear_inputs = []
+        self.inputs = []
+        self.outputs = []
+        self.input_blinds = []
+        self.output_blinds = []
+
+        self.ec = ec
+
+    def add_clear_input(self, value, token_id, signature_secret):
+        clear_input = ClassNamespace()
+        clear_input.value = value
+        clear_input.token_id = token_id
+        clear_input.signature_secret = signature_secret
+        self.clear_inputs.append(clear_input)
+
+    def add_input(self, all_coins, secret, note, user_data_blind):
+        input = ClassNamespace()
+        input.all_coins = all_coins
+        input.secret = secret
+        input.note = note
+        input.user_data_blind = user_data_blind
+        self.inputs.append(input)
+
+    def add_output(self, value, token_id, public, spend_hook, user_data):
+        output = ClassNamespace()
+        output.value = value
+        output.token_id = token_id
+        output.public = public
+        output.spend_hook = spend_hook
+        output.user_data = user_data
+        self.outputs.append(output)
+
+    def compute_remainder_blind(self, clear_inputs, input_blinds,
+                                output_blinds):
+        total = 0
+        total += sum(input.value_blind for input in clear_inputs)
+        total += sum(input_blinds)
+        total -= sum(output_blinds)
+        return total % self.ec.order
+
+    def build(self):
+        tx = SendPaymentTx(self.ec)
+        token_blind = self.ec.random_scalar()
+
+        for input in self.clear_inputs:
+            tx_clear_input = ClassNamespace()
+            tx_clear_input.__name__ = "TransactionClearInput"
+            tx_clear_input.value = input.value
+            tx_clear_input.token_id = input.token_id
+            tx_clear_input.value_blind = self.ec.random_scalar()
+            tx_clear_input.token_blind = token_blind
+            tx_clear_input.signature_public = self.ec.multiply(
+                input.signature_secret, self.ec.G)
+            tx.clear_inputs.append(tx_clear_input)
+
+        self.input_blinds = []
+        signature_secrets = []
+        for input in self.inputs:
+            value_blind = self.ec.random_scalar()
+            self.input_blinds.append(value_blind)
+
+            signature_secret = self.ec.random_scalar()
+            signature_secrets.append(signature_secret)
+
+            tx_input = ClassNamespace()
+            tx_input.__name__ = "TransactionInput"
+            tx_input.burn_proof = BurnProof(
+                input.note.value, input.note.token_id, value_blind,
+                token_blind, input.note.serial, input.note.coin_blind,
+                input.secret, input.note.spend_hook, input.note.user_data,
+                input.user_data_blind, input.all_coins, signature_secret,
+                self.ec)
+            tx_input.revealed = tx_input.burn_proof.get_revealed()
+            tx.inputs.append(tx_input)
+
+        assert self.outputs
+        self.output_blinds = []
+        for i, output in enumerate(self.outputs):
+            if i == len(self.outputs) - 1:
+                value_blind = self.compute_remainder_blind(
+                    tx.clear_inputs, self.input_blinds, self.output_blinds)
+            else:
+                value_blind = self.ec.random_scalar()
+            self.output_blinds.append(value_blind)
+
+            note = ClassNamespace()
+            note.serial = self.ec.random_base()
+            note.value = output.value
+            note.token_id = output.token_id
+            note.coin_blind = self.ec.random_base()
+            note.value_blind = value_blind
+            note.token_blind = token_blind
+            note.spend_hook = output.spend_hook
+            note.user_data = output.user_data
+
+            tx_output = ClassNamespace()
+            tx_output.__name__ = "TransactionOutput"
+
+            tx_output.mint_proof = MintProof(
+                note.value, note.token_id, note.value_blind,
+                note.token_blind, note.serial, note.coin_blind,
+                output.public, output.spend_hook, output.user_data, self.ec)
+            tx_output.revealed = tx_output.mint_proof.get_revealed()
+            assert tx_output.mint_proof.verify(tx_output.revealed)
+
+            # Is normally encrypted
+            tx_output.enc_note = note
+            tx_output.enc_note.__name__ = "TransactionOutputEncryptedNote"
+
+            tx.outputs.append(tx_output)
+
+        unsigned_tx_data = tx.partial_encode()
+        for (input, info) in zip(tx.clear_inputs, self.clear_inputs):
+            secret = info.signature_secret
+            signature = sign(unsigned_tx_data, secret, self.ec)
+            input.signature = signature
+        for (input, signature_secret) in zip(tx.inputs, signature_secrets):
+            signature = sign(unsigned_tx_data, signature_secret, self.ec)
+            input.signature = signature
+
+        return tx
+
+# Transaction representing Money::send_payment() function call
+class SendPaymentTx:
+
+    def __init__(self, ec):
+        self.clear_inputs = []
+        self.inputs = []
+        self.outputs = []
+
+        self.ec = ec
+
+    def partial_encode(self):
+        # There is no cake
+        return b"hello"
+
+    def verify(self):
+        if not self._check_value_commits():
+            return False, "value commits do not match"
+
+        if not self._check_proofs():
+            return False, "proofs failed to verify"
+
+        if not self._verify_token_commitments():
+            return False, "token ID mismatch"
+
+        unsigned_tx_data = self.partial_encode()
+        for input in self.clear_inputs:
+            public = input.signature_public
+            if not verify(unsigned_tx_data, input.signature, public, self.ec):
+                return False
+        for input in self.inputs:
+            public = input.revealed.signature_public
+            if not verify(unsigned_tx_data, input.signature, public, self.ec):
+                return False
+
+        return True, None
+
+    def _check_value_commits(self):
+        valcom_total = (0, 1, 0)
+
+        for input in self.clear_inputs:
+            value_commit = pedersen_encrypt(input.value, input.value_blind,
+                                            self.ec)
+            valcom_total = self.ec.add(valcom_total, value_commit)
+        for input in self.inputs:
+            value_commit = input.revealed.value_commit
+            valcom_total = self.ec.add(valcom_total, value_commit)
+        for output in self.outputs:
+            v = output.revealed.value_commit
+            value_commit = (v[0], -v[1], v[2])
+            valcom_total = self.ec.add(valcom_total, value_commit)
+
+        return valcom_total == (0, 1, 0)
+
+    def _check_proofs(self):
+        for input in self.inputs:
+            if not input.burn_proof.verify(input.revealed):
+                return False
+        for output in self.outputs:
+            if not output.mint_proof.verify(output.revealed):
+                return False
+        return True
+
+    def _verify_token_commitments(self):
+        assert len(self.outputs) > 0
+        token_commit_value = self.outputs[0].revealed.token_commit
+        for input in self.clear_inputs:
+            token_commit = pedersen_encrypt(input.token_id, input.token_blind,
+                                            self.ec)
+            if token_commit != token_commit_value:
+                return False
+        for input in self.inputs:
+            if input.revealed.token_commit != token_commit_value:
+                return False
+        for output in self.outputs:
+            if output.revealed.token_commit != token_commit_value:
+                return False
+        return True
+
+class BurnProof:
+
+    def __init__(self, value, token_id, value_blind, token_blind, serial,
+                 coin_blind, secret, spend_hook, user_data, user_data_blind,
+                 all_coins, signature_secret, ec):
+        self.value = value
+        self.token_id = token_id
+        self.value_blind = value_blind
+        self.token_blind = token_blind
+        self.serial = serial
+        self.coin_blind = coin_blind
+        self.secret = secret
+        self.spend_hook = spend_hook
+        self.user_data = user_data
+        self.user_data_blind = user_data_blind
+        self.all_coins = all_coins
+        self.signature_secret = signature_secret
+
+        self.ec = ec
+
+    def get_revealed(self):
+        revealed = ClassNamespace()
+        revealed.nullifier = ff_hash(self.ec.p, self.secret, self.serial)
+
+        revealed.value_commit = pedersen_encrypt(
+            self.value, self.value_blind, self.ec
+        )
+        revealed.token_commit = pedersen_encrypt(
+            self.token_id, self.token_blind, self.ec
+        )
+
+        # is_valid_merkle_root()
+        revealed.all_coins = self.all_coins
+
+        revealed.signature_public = self.ec.multiply(self.signature_secret,
+                                                     self.ec.G)
+
+        # This is fully public, no merkle tree or anything
+        revealed.spend_hook = self.spend_hook
+
+        # Re-export user_data field for access by other contracts
+        revealed.enc_user_data = ff_hash(
+            self.ec.p,
+            self.user_data,
+            self.user_data_blind
+        )
+
+        return revealed
+
+    def verify(self, public):
+        revealed = self.get_revealed()
+
+        public_key = self.ec.multiply(self.secret, self.ec.G)
+        coin = ff_hash(
+            self.ec.p,
+            public_key[0],
+            public_key[1],
+            self.value,
+            self.token_id,
+            self.serial,
+            self.coin_blind,
+            self.spend_hook,
+            self.user_data,
+        )
+        # Merkle root check
+        if coin not in self.all_coins:
+            return False
+
+        return all([
+            revealed.nullifier == public.nullifier,
+            revealed.value_commit == public.value_commit,
+            revealed.token_commit == public.token_commit,
+            revealed.all_coins == public.all_coins,
+            revealed.signature_public == public.signature_public,
+            revealed.enc_user_data == public.enc_user_data
+        ])
+
+class MintProof:
+
+    def __init__(self, value, token_id, value_blind, token_blind, serial,
+                 coin_blind, public, spend_hook, user_data, ec):
+        self.value = value
+        self.token_id = token_id
+        self.value_blind = value_blind
+        self.token_blind = token_blind
+        self.serial = serial
+        self.coin_blind = coin_blind
+        self.public = public
+        self.spend_hook = spend_hook
+        self.user_data = user_data
+
+        self.ec = ec
+
+    def get_revealed(self):
+        revealed = ClassNamespace()
+        revealed.coin = ff_hash(
+            self.ec.p,
+            self.public[0],
+            self.public[1],
+            self.value,
+            self.token_id,
+            self.serial,
+            self.coin_blind,
+            self.spend_hook,
+            self.user_data
+        )
+
+        revealed.value_commit = pedersen_encrypt(
+            self.value, self.value_blind, self.ec
+        )
+        revealed.token_commit = pedersen_encrypt(
+            self.token_id, self.token_blind, self.ec
+        )
+
+        return revealed
+
+    def verify(self, public):
+        revealed = self.get_revealed()
+        return all([
+            revealed.coin == public.coin,
+            revealed.value_commit == public.value_commit,
+            revealed.token_commit == public.token_commit,
+        ])
+