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