Files
darkfi/example/dao/schema/main.py

1464 lines
47 KiB
Python

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