mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
1464 lines
47 KiB
Python
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))
|
|
|