diff --git a/bin/daod/demo/main.py b/bin/daod/demo/main.py index 6af3c563f..41a791c78 100644 --- a/bin/daod/demo/main.py +++ b/bin/daod/demo/main.py @@ -63,13 +63,8 @@ class ProposerTxBuilder: input.note = note self.inputs.append(input) - def set_dao(self, proposer_limit, quorum, approval_ratio, - gov_token_id, dao_bulla_blind): - self.dao_proposer_limit = proposer_limit - self.dao_quorum = quorum - self.dao_approval_ratio = approval_ratio - self.gov_token_id = gov_token_id - self.dao_bulla_blind = dao_bulla_blind + def set_dao(self, dao): + self.dao = dao def build(self): tx = ProposerTx(self.ec) @@ -88,11 +83,12 @@ class ProposerTxBuilder: total_value, total_value_blinds, # DAO params - self.dao_proposer_limit, - self.dao_quorum, - self.dao_approval_ratio, - self.gov_token_id, - self.dao_bulla_blind, + 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 @@ -261,7 +257,7 @@ class ProposerTxDaoProof: def __init__(self, total_value, total_value_blinds, proposer_limit, quorum, approval_ratio, - gov_token_id, dao_bulla_blind, + 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, @@ -272,6 +268,7 @@ class ProposerTxDaoProof: 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 @@ -300,6 +297,8 @@ class ProposerTxDaoProof: 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) @@ -327,12 +326,16 @@ class ProposerTxDaoProof: 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 + assert self.proposal_amount > 0 + # # total_value >= proposer_limit # @@ -587,11 +590,12 @@ class VoteTx: class DaoBuilder: def __init__(self, proposer_limit, quorum, approval_ratio, - gov_token_id, dao_bulla_blind, ec): + 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 @@ -602,6 +606,7 @@ class DaoBuilder: self.quorum, self.approval_ratio, self.gov_token_id, + self.dao_public_key, self.dao_bulla_blind, self.ec ) @@ -627,11 +632,12 @@ class Dao: class DaoMintProof: def __init__(self, proposer_limit, quorum, approval_ratio, - gov_token_id, dao_bulla_blind, ec): + 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 @@ -644,6 +650,8 @@ class DaoMintProof: self.quorum, self.approval_ratio, self.gov_token_id, + self.dao_public_key[0], + self.dao_public_key[1], self.dao_bulla_blind ) @@ -668,11 +676,14 @@ class DaoState: 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): - pass + self.proposal_nullifiers.add(update.proposal_nullifier) def apply(self, update): self.dao_bullas.add(update.bulla) @@ -700,6 +711,12 @@ class DaoExecBuilder: 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 @@ -709,6 +726,12 @@ class DaoExecBuilder: 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 @@ -722,6 +745,12 @@ class DaoExecBuilder: 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() @@ -750,6 +779,12 @@ class DaoExecProof: 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 @@ -759,6 +794,12 @@ class DaoExecProof: 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 @@ -766,10 +807,6 @@ class DaoExecProof: revealed = ClassNamespace() # Corresponds to proposals merkle root revealed.all_proposals = self.all_proposals - return revealed - - def verify(self, public): - revealed = self.get_revealed() dao_bulla = crypto.ff_hash( self.ec.p, @@ -777,6 +814,76 @@ class DaoExecProof: 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( @@ -792,10 +899,21 @@ class DaoExecProof: # This being true also implies the DAO is valid assert proposal_bulla in self.all_proposals + assert self.total_votes >= self.dao.quorum + + assert self.win_votes >= self.dao.approval_ratio + 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): +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) @@ -805,8 +923,33 @@ def dao_exec_state_transition(state, tx): 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 = ... + update.proposal_nullifier = tx.revealed.proposal_nullifier return update # contract interface functions @@ -901,6 +1044,7 @@ def main(argv): dao_quorum, dao_approval_ratio, gov_token_id, + dao_public_key, dao_bulla_blind, ec ) @@ -940,6 +1084,10 @@ def main(argv): 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 @@ -1046,16 +1194,18 @@ def main(argv): # 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_proposer_limit, - dao_quorum, - dao_approval_ratio, - gov_token_id, - dao_bulla_blind - ) + builder.set_dao(dao) tx = builder.build() # No state changes actually happen so ignore the update @@ -1223,9 +1373,13 @@ def main(argv): 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 @@ -1249,12 +1403,13 @@ def main(argv): # - verifier: check sum of vote_commits is correct # - win_votes / total_votes >= approval_ratio - 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.bulla_blind = dao_bulla_blind + 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, @@ -1264,13 +1419,23 @@ def main(argv): 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)) is None: + 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__": diff --git a/bin/daod/demo/money.py b/bin/daod/demo/money.py index 05446749f..49c9f905f 100644 --- a/bin/daod/demo/money.py +++ b/bin/daod/demo/money.py @@ -8,6 +8,8 @@ class SendPaymentTxBuilder: self.clear_inputs = [] self.inputs = [] self.outputs = [] + self.input_blinds = [] + self.output_blinds = [] self.ec = ec @@ -58,11 +60,11 @@ class SendPaymentTxBuilder: input.signature_secret, self.ec.G) tx.clear_inputs.append(tx_clear_input) - input_blinds = [] + self.input_blinds = [] signature_secrets = [] for input in self.inputs: value_blind = self.ec.random_scalar() - input_blinds.append(value_blind) + self.input_blinds.append(value_blind) signature_secret = self.ec.random_scalar() signature_secrets.append(signature_secret) @@ -79,14 +81,14 @@ class SendPaymentTxBuilder: tx.inputs.append(tx_input) assert self.outputs - output_blinds = [] + self.output_blinds = [] for i, output in enumerate(self.outputs): if i == len(self.outputs) - 1: value_blind = self.compute_remainder_blind( - tx.clear_inputs, input_blinds, output_blinds) + tx.clear_inputs, self.input_blinds, self.output_blinds) else: value_blind = self.ec.random_scalar() - output_blinds.append(value_blind) + self.output_blinds.append(value_blind) note = ClassNamespace() note.serial = self.ec.random_base()