diff --git a/Cargo.toml b/Cargo.toml index ec1e7d6c1..602fe25f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -360,8 +360,8 @@ required-features = ["crypto"] [[example]] name = "dao" -path = "example/dao/dao.rs" -required-features = ["crypto"] +path = "example/dao/src/dao.rs" +required-features = ["crypto", "rpc"] [[example]] name = "lead" diff --git a/example/dao/Cargo.toml b/example/dao/Cargo.toml new file mode 100644 index 000000000..2d787c6a7 --- /dev/null +++ b/example/dao/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "dao" +version = "0.3.0" +edition = "2021" + +[dependencies] +darkfi = {path = "../../", features = ["rpc", "crypto", "tx", "node"]} +darkfi-serial = {path = "../../src/serial"} + +# Async +smol = "1.2.5" +futures = "0.3.24" +async-std = {version = "1.12.0", features = ["attributes"]} +async-trait = "0.1.57" +async-channel = "1.7.1" +async-executor = "1.4.1" +easy-parallel = "3.2.0" + +# Misc +log = "0.4.17" +num_cpus = "1.13.1" +simplelog = "0.12.0" +thiserror = "1.0.37" + +# Crypto +incrementalmerkletree = "0.3.0" +pasta_curves = "0.4.0" +halo2_gadgets = "0.2.0" +halo2_proofs = "0.2.0" +rand = "0.8.5" +crypto_api_chachapoly = "0.5.0" +group = "0.12.0" + +# Encoding and parsing +serde_json = "1.0.85" +bs58 = "0.4.0" +fxhash = "0.2.1" + +# Utilities +lazy_static = "1.4.0" +url = "2.3.1" diff --git a/example/dao/contract/mod.rs b/example/dao/contract/mod.rs deleted file mode 100644 index 94261f732..000000000 --- a/example/dao/contract/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod dao_contract; -pub mod example_contract; -pub mod money_contract; diff --git a/example/dao/proof/dao-exec.zk b/example/dao/proof/dao-exec.zk index e5c41f68c..9fa9c199e 100644 --- a/example/dao/proof/dao-exec.zk +++ b/example/dao/proof/dao-exec.zk @@ -91,7 +91,7 @@ circuit "DaoExec" { proposal_token_id, dao_serial, dao_spend_hook, - proposal_bulla, + dao_bulla, dao_coin_blind, ); constrain_instance(coin_1); diff --git a/example/dao/schema/classnamespace.py b/example/dao/schema/classnamespace.py deleted file mode 100644 index 858a888d6..000000000 --- a/example/dao/schema/classnamespace.py +++ /dev/null @@ -1,46 +0,0 @@ -from types import SimpleNamespace - -class ClassNamespace(SimpleNamespace): - def __init__(self, dic=None): - if dic is None: - return - # if type(dic) is dict: - for key in dic: - setattr(self, key, self.envelop(dic[key])) - # else: - # raise CatalogError("ClassNamespace AIUTO!") - - def envelop(self, elem): - if type(elem) is dict: - return ClassNamespace(elem) - elif type(elem) is list: - return [self.envelop(x) for x in elem] - else: - return elem - - # if d is not None: - # for key in d: - # if type(d[key]) is dict: - # setattr(self, key, ClassNamespace(d[key])) - # else: - # setattr(self, key, d[key]) - - def __contains__(self, x): - return x in self.__dict__ - - def __json__(self, x): - return self.__dict__ - - def copy(self): - return self.__dict__.copy() - - def classcopy(self): - dummy = ClassNamespace() - dummy.__dict__.update(self.__dict__) - return dummy - - def dictcopy(self): - return self.__dict__.copy() - - def update(self, oth): - self.__dict__.update(oth.__dict__) diff --git a/example/dao/schema/crypto.py b/example/dao/schema/crypto.py deleted file mode 100644 index e90ace858..000000000 --- a/example/dao/schema/crypto.py +++ /dev/null @@ -1,158 +0,0 @@ -import hashlib -import random - -def ff_inv(a, p): - a %= p - - # extended euclidean algorithm - # ps + at = 1 - t = 0 - new_t = 1 - r = p - new_r = a - - while new_r != 0: - quotient = r // new_r - t, new_t = new_t, t - quotient * new_t - r, new_r = new_r, r - quotient * new_r - - assert r == 1 - if t < 0: - t += p - - return t - -class EllipticCurve: - - def __init__(self, p, A, B, order, G, H, J): - self.p = p - self.A = A - self.B = B - self.order = order - self.G = G - self.H = H - self.J = J - assert self.is_valid(G) - assert self.is_valid(H) - - def is_valid(self, P): - x, y, z = P - if z == 0: - return x != 0 or y != 0 - z_inv = ff_inv(z, self.p) - x, y = x * z_inv, y * z_inv - return y**2 % self.p == (x**3 + self.A * x + self.B) % self.p - - def add(self, p1, p2): - x1, y1, z1 = p1 - x2, y2, z2 = p2 - - if z1 == 0: - return (x2, y2, z2) - elif z2 == 0: - return (x1, y1, z1) - - if x1 == x2: - if y1 != y2: - return (0, 1, 0) - - assert y1 != 0 - m = (3 * x1**2 + self.A) * ff_inv(2*y1, self.p) - else: - m = (y2 - y1) * ff_inv(x2 - x1, self.p) - - x3 = (m**2 - x1 - x2) % self.p - y3 = (m * (x1 - x3) - y1) % self.p - return (x3, y3, 1) - - def multiply(self, m, p): - bits = f"{m:b}" - result = (0, 1, 0) - temp = p - for bit in bits[::-1]: - if bit == "1": - result = self.add(result, temp) - temp = self.add(temp, temp) - return result - - def random_point(self): - m = self.random_scalar() - return self.multiply(m, self.G) - - def random_scalar(self): - m = random.randrange(0, self.order - 1) - return m - - def random_base(self): - m = random.randrange(0, self.p - 1) - return m - -def pallas_curve(): - # Pallas - p = 0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001 - q = 0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001 - G = (5, 5392431450607408583390510508521091931943415030464003135511088002453056875732, 1) - H = (9762257241998025279988087154025308614062019274413483967640476725944341089207, - 12058632856930756995627167820351407063813260358041446014729496773111030695755, 1) - J = (7795559447963065356059848000022900528974048197507738248625163674930282081839, - 5156492880772775379342191094371887365795329446468828588866320184016504353483, 1) - ec = EllipticCurve(p, 0, 5, q, G, H, J) - A = (144931808354919915876542440378319484704499556634959420306426167479163065488, - 2699682121356767698440748624399854659825391162912545787181017961871465868196, 1) - B = (16017037670495191561606513965775243786961447026019262496667491008912834496943, - 20395164507282344548629891414360366999207473153143014512687861307997120664849, 1) - assert ec.add(A, B) == (2414658659502531855741199170408914396997834981355655923471364687102714431309, 21133344194418979683767005688724798091220515434220043854575260979109407444719, 1) - m = 26322809409216846271933211244226061368157231119725763192402071651286829040466 - assert ec.multiply(m, G) == (15862887453366837597569434439063150886012590021428640083047997467990450633825, 25887284719793568129480941070850220101898092026705204234126448799557008384178, 1) - return ec - -def pedersen_encrypt(x, y, ec): - vcv = ec.multiply(x, ec.G) - vcr = ec.multiply(y, ec.H) - return ec.add(vcv, vcr) - -def _add_to_hasher(hasher, args): - for arg in args: - match arg: - case int() as arg: - hasher.update(arg.to_bytes(32, byteorder="little")) - case bytes() as arg: - hasher.update(arg) - case list() as arg: - _add_to_hasher(hasher, arg) - case _: - raise Exception(f"unknown hash arg '{arg}' type: {type(arg)}") - -def ff_hash(p, *args): - hasher = hashlib.sha256() - _add_to_hasher(hasher, args) - value = int.from_bytes(hasher.digest(), byteorder="little") - return value % p - -def hash_point(point, message=None): - hasher = hashlib.sha256() - for x_i in point: - hasher.update(x_i.to_bytes(32, byteorder="little")) - # Optional message - if message is not None: - hasher.update(message) - value = int.from_bytes(hasher.digest(), byteorder="little") - return value - -def sign(message, secret, ec): - ephem_secret = ec.random_scalar() - ephem_public = ec.multiply(ephem_secret, ec.G) - challenge = hash_point(ephem_public, message) % ec.order - response = (ephem_secret + challenge * secret) % ec.order - return ephem_public, response - -def verify(message, signature, public, ec): - ephem_public, response = signature - challenge = hash_point(ephem_public, message) % ec.order - # sG - lhs = ec.multiply(response, ec.G) - # R + cP - rhs_cP = ec.multiply(challenge, public) - rhs = ec.add(ephem_public, rhs_cP) - return lhs == rhs - diff --git a/example/dao/schema/main.py b/example/dao/schema/main.py deleted file mode 100644 index ca3e16974..000000000 --- a/example/dao/schema/main.py +++ /dev/null @@ -1,1463 +0,0 @@ -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)) - diff --git a/example/dao/schema/money.py b/example/dao/schema/money.py deleted file mode 100644 index ec62934c9..000000000 --- a/example/dao/schema/money.py +++ /dev/null @@ -1,331 +0,0 @@ -from classnamespace import ClassNamespace -from crypto import ff_hash, pedersen_encrypt, sign, verify - -# Tx representing send_payment() contract call -class SendPaymentTxBuilder: - - def __init__(self, ec): - self.clear_inputs = [] - self.inputs = [] - self.outputs = [] - self.input_blinds = [] - self.output_blinds = [] - - self.ec = ec - - def add_clear_input(self, value, token_id, signature_secret): - clear_input = ClassNamespace() - clear_input.value = value - clear_input.token_id = token_id - clear_input.signature_secret = signature_secret - self.clear_inputs.append(clear_input) - - def add_input(self, all_coins, secret, note, user_data_blind): - input = ClassNamespace() - input.all_coins = all_coins - input.secret = secret - input.note = note - input.user_data_blind = user_data_blind - self.inputs.append(input) - - def add_output(self, value, token_id, public, spend_hook, user_data): - output = ClassNamespace() - output.value = value - output.token_id = token_id - output.public = public - output.spend_hook = spend_hook - output.user_data = user_data - self.outputs.append(output) - - def compute_remainder_blind(self, clear_inputs, input_blinds, - output_blinds): - total = 0 - total += sum(input.value_blind for input in clear_inputs) - total += sum(input_blinds) - total -= sum(output_blinds) - return total % self.ec.order - - def build(self): - tx = SendPaymentTx(self.ec) - token_blind = self.ec.random_scalar() - - for input in self.clear_inputs: - tx_clear_input = ClassNamespace() - tx_clear_input.__name__ = "TransactionClearInput" - tx_clear_input.value = input.value - tx_clear_input.token_id = input.token_id - tx_clear_input.value_blind = self.ec.random_scalar() - tx_clear_input.token_blind = token_blind - tx_clear_input.signature_public = self.ec.multiply( - input.signature_secret, self.ec.G) - tx.clear_inputs.append(tx_clear_input) - - self.input_blinds = [] - signature_secrets = [] - for input in self.inputs: - value_blind = self.ec.random_scalar() - self.input_blinds.append(value_blind) - - signature_secret = self.ec.random_scalar() - signature_secrets.append(signature_secret) - - tx_input = ClassNamespace() - tx_input.__name__ = "TransactionInput" - tx_input.burn_proof = BurnProof( - input.note.value, input.note.token_id, value_blind, - token_blind, input.note.serial, input.note.coin_blind, - input.secret, input.note.spend_hook, input.note.user_data, - input.user_data_blind, input.all_coins, signature_secret, - self.ec) - tx_input.revealed = tx_input.burn_proof.get_revealed() - tx.inputs.append(tx_input) - - assert self.outputs - self.output_blinds = [] - for i, output in enumerate(self.outputs): - if i == len(self.outputs) - 1: - value_blind = self.compute_remainder_blind( - tx.clear_inputs, self.input_blinds, self.output_blinds) - else: - value_blind = self.ec.random_scalar() - self.output_blinds.append(value_blind) - - note = ClassNamespace() - note.serial = self.ec.random_base() - note.value = output.value - note.token_id = output.token_id - note.coin_blind = self.ec.random_base() - note.value_blind = value_blind - note.token_blind = token_blind - note.spend_hook = output.spend_hook - note.user_data = output.user_data - - tx_output = ClassNamespace() - tx_output.__name__ = "TransactionOutput" - - tx_output.mint_proof = MintProof( - note.value, note.token_id, note.value_blind, - note.token_blind, note.serial, note.coin_blind, - output.public, output.spend_hook, output.user_data, self.ec) - tx_output.revealed = tx_output.mint_proof.get_revealed() - assert tx_output.mint_proof.verify(tx_output.revealed) - - # Is normally encrypted - tx_output.enc_note = note - tx_output.enc_note.__name__ = "TransactionOutputEncryptedNote" - - tx.outputs.append(tx_output) - - unsigned_tx_data = tx.partial_encode() - for (input, info) in zip(tx.clear_inputs, self.clear_inputs): - secret = info.signature_secret - signature = sign(unsigned_tx_data, secret, self.ec) - input.signature = signature - for (input, signature_secret) in zip(tx.inputs, signature_secrets): - signature = sign(unsigned_tx_data, signature_secret, self.ec) - input.signature = signature - - return tx - -# Transaction representing Money::send_payment() function call -class SendPaymentTx: - - def __init__(self, ec): - self.clear_inputs = [] - self.inputs = [] - self.outputs = [] - - self.ec = ec - - def partial_encode(self): - # There is no cake - return b"hello" - - def verify(self): - if not self._check_value_commits(): - return False, "value commits do not match" - - if not self._check_proofs(): - return False, "proofs failed to verify" - - if not self._verify_token_commitments(): - return False, "token ID mismatch" - - unsigned_tx_data = self.partial_encode() - for input in self.clear_inputs: - public = input.signature_public - if not verify(unsigned_tx_data, input.signature, public, self.ec): - return False - for input in self.inputs: - public = input.revealed.signature_public - if not verify(unsigned_tx_data, input.signature, public, self.ec): - return False - - return True, None - - def _check_value_commits(self): - valcom_total = (0, 1, 0) - - for input in self.clear_inputs: - value_commit = pedersen_encrypt(input.value, input.value_blind, - self.ec) - valcom_total = self.ec.add(valcom_total, value_commit) - for input in self.inputs: - value_commit = input.revealed.value_commit - valcom_total = self.ec.add(valcom_total, value_commit) - for output in self.outputs: - v = output.revealed.value_commit - value_commit = (v[0], -v[1], v[2]) - valcom_total = self.ec.add(valcom_total, value_commit) - - return valcom_total == (0, 1, 0) - - def _check_proofs(self): - for input in self.inputs: - if not input.burn_proof.verify(input.revealed): - return False - for output in self.outputs: - if not output.mint_proof.verify(output.revealed): - return False - return True - - def _verify_token_commitments(self): - assert len(self.outputs) > 0 - token_commit_value = self.outputs[0].revealed.token_commit - for input in self.clear_inputs: - token_commit = pedersen_encrypt(input.token_id, input.token_blind, - self.ec) - if token_commit != token_commit_value: - return False - for input in self.inputs: - if input.revealed.token_commit != token_commit_value: - return False - for output in self.outputs: - if output.revealed.token_commit != token_commit_value: - return False - return True - -class BurnProof: - - def __init__(self, value, token_id, value_blind, token_blind, serial, - coin_blind, secret, spend_hook, user_data, user_data_blind, - all_coins, signature_secret, ec): - self.value = value - self.token_id = token_id - self.value_blind = value_blind - self.token_blind = token_blind - self.serial = serial - self.coin_blind = coin_blind - self.secret = secret - self.spend_hook = spend_hook - self.user_data = user_data - self.user_data_blind = user_data_blind - self.all_coins = all_coins - self.signature_secret = signature_secret - - self.ec = ec - - def get_revealed(self): - revealed = ClassNamespace() - revealed.nullifier = ff_hash(self.ec.p, self.secret, self.serial) - - revealed.value_commit = pedersen_encrypt( - self.value, self.value_blind, self.ec - ) - revealed.token_commit = pedersen_encrypt( - self.token_id, self.token_blind, self.ec - ) - - # is_valid_merkle_root() - revealed.all_coins = self.all_coins - - revealed.signature_public = self.ec.multiply(self.signature_secret, - self.ec.G) - - # This is fully public, no merkle tree or anything - revealed.spend_hook = self.spend_hook - - # Re-export user_data field for access by other contracts - revealed.enc_user_data = ff_hash( - self.ec.p, - self.user_data, - self.user_data_blind - ) - - return revealed - - def verify(self, public): - revealed = self.get_revealed() - - public_key = self.ec.multiply(self.secret, self.ec.G) - coin = ff_hash( - self.ec.p, - public_key[0], - public_key[1], - self.value, - self.token_id, - self.serial, - self.coin_blind, - self.spend_hook, - self.user_data, - ) - # Merkle root check - if coin not in self.all_coins: - return False - - return all([ - revealed.nullifier == public.nullifier, - revealed.value_commit == public.value_commit, - revealed.token_commit == public.token_commit, - revealed.all_coins == public.all_coins, - revealed.signature_public == public.signature_public, - revealed.enc_user_data == public.enc_user_data - ]) - -class MintProof: - - def __init__(self, value, token_id, value_blind, token_blind, serial, - coin_blind, public, spend_hook, user_data, ec): - self.value = value - self.token_id = token_id - self.value_blind = value_blind - self.token_blind = token_blind - self.serial = serial - self.coin_blind = coin_blind - self.public = public - self.spend_hook = spend_hook - self.user_data = user_data - - self.ec = ec - - def get_revealed(self): - revealed = ClassNamespace() - revealed.coin = ff_hash( - self.ec.p, - self.public[0], - self.public[1], - self.value, - self.token_id, - self.serial, - self.coin_blind, - self.spend_hook, - self.user_data - ) - - revealed.value_commit = pedersen_encrypt( - self.value, self.value_blind, self.ec - ) - revealed.token_commit = pedersen_encrypt( - self.token_id, self.token_blind, self.ec - ) - - return revealed - - def verify(self, public): - revealed = self.get_revealed() - return all([ - revealed.coin == public.coin, - revealed.value_commit == public.value_commit, - revealed.token_commit == public.token_commit, - ]) - diff --git a/example/dao/contract/dao_contract/exec/mod.rs b/example/dao/src/contract/dao/exec/mod.rs similarity index 100% rename from example/dao/contract/dao_contract/exec/mod.rs rename to example/dao/src/contract/dao/exec/mod.rs diff --git a/example/dao/contract/dao_contract/exec/validate.rs b/example/dao/src/contract/dao/exec/validate.rs similarity index 89% rename from example/dao/contract/dao_contract/exec/validate.rs rename to example/dao/src/contract/dao/exec/validate.rs index 047eca12d..9c8a52c6f 100644 --- a/example/dao/contract/dao_contract/exec/validate.rs +++ b/example/dao/src/contract/dao/exec/validate.rs @@ -1,4 +1,5 @@ -use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; +use std::any::{Any, TypeId}; + use pasta_curves::{ arithmetic::CurveAffine, group::{Curve, Group}, @@ -9,11 +10,10 @@ use darkfi::{ crypto::{coin::Coin, keypair::PublicKey, types::DrkCircuitField}, Error as DarkFiError, }; - -use std::any::{Any, TypeId}; +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; use crate::{ - contract::{dao_contract, dao_contract::CONTRACT_ID, money_contract}, + contract::{dao, dao::CONTRACT_ID, money}, util::{CallDataBase, HashableBase, StateRegistry, Transaction, UpdateBase}, }; @@ -100,7 +100,7 @@ impl CallDataBase for CallData { fn encode_bytes( &self, mut writer: &mut dyn std::io::Write, - ) -> core::result::Result { + ) -> std::result::Result { self.encode(&mut writer) } } @@ -113,7 +113,7 @@ pub fn state_transition( let func_call = &parent_tx.func_calls[func_call_index]; let call_data = func_call.call_data.as_any(); - assert_eq!((*call_data).type_id(), TypeId::of::()); + assert_eq!((&*call_data).type_id(), TypeId::of::()); let call_data = call_data.downcast_ref::(); // This will be inside wasm so unwrap is fine. @@ -131,17 +131,17 @@ pub fn state_transition( } // 3. First item should be a Money::transfer() calldata - if parent_tx.func_calls[0].func_id != *money_contract::transfer::FUNC_ID { + if parent_tx.func_calls[0].func_id != *money::transfer::FUNC_ID { return Err(Error::InvalidCallData) } let money_transfer_call_data = parent_tx.func_calls[0].call_data.as_any(); let money_transfer_call_data = - money_transfer_call_data.downcast_ref::(); + money_transfer_call_data.downcast_ref::(); let money_transfer_call_data = money_transfer_call_data.unwrap(); assert_eq!( money_transfer_call_data.type_id(), - TypeId::of::() + TypeId::of::() ); // 4. Money::transfer() has exactly 2 outputs @@ -168,9 +168,8 @@ pub fn state_transition( } // 3. get the ProposalVote from DAO::State - let state = states - .lookup::(*CONTRACT_ID) - .expect("Return type is not of type State"); + let state = + states.lookup::(*CONTRACT_ID).expect("Return type is not of type State"); let proposal_votes = state.proposal_votes.get(&HashableBase(call_data.proposal)).unwrap(); // 4. check yes_votes_commit is the same as in ProposalVote @@ -193,7 +192,7 @@ pub struct Update { impl UpdateBase for Update { fn apply(self: Box, states: &mut StateRegistry) { let state = states - .lookup_mut::(*CONTRACT_ID) + .lookup_mut::(*CONTRACT_ID) .expect("Return type is not of type State"); state.proposal_votes.remove(&HashableBase(self.proposal)).unwrap(); } diff --git a/example/dao/contract/dao_contract/exec/wallet.rs b/example/dao/src/contract/dao/exec/wallet.rs similarity index 96% rename from example/dao/contract/dao_contract/exec/wallet.rs rename to example/dao/src/contract/dao/exec/wallet.rs index 4d55cec10..eedba67c7 100644 --- a/example/dao/contract/dao_contract/exec/wallet.rs +++ b/example/dao/src/contract/dao/exec/wallet.rs @@ -1,8 +1,7 @@ -use log::debug; -use rand::rngs::OsRng; - use halo2_proofs::circuit::Value; +use log::debug; use pasta_curves::{arithmetic::CurveAffine, group::Curve, pallas}; +use rand::rngs::OsRng; use darkfi::{ crypto::{ @@ -14,7 +13,7 @@ use darkfi::{ }; use crate::{ - contract::dao_contract::{ + contract::dao::{ exec::validate::CallData, mint::wallet::DaoParams, propose::wallet::Proposal, CONTRACT_ID, }, util::{FuncCall, ZkContractInfo, ZkContractTable}, @@ -40,7 +39,6 @@ pub struct Builder { impl Builder { pub fn build(self, zk_bins: &ZkContractTable) -> FuncCall { debug!(target: "dao_contract::exec::wallet::Builder", "build()"); - debug!(target: "dao_contract::exec::wallet", "proposalserial{:?}", self.proposal.serial); let mut proofs = vec![]; let proposal_dest_coords = self.proposal.dest.0.to_affine().coordinates().unwrap(); @@ -100,7 +98,7 @@ impl Builder { self.proposal.token_id, self.dao_serial, self.hook_dao_exec, - proposal_bulla, + dao_bulla, self.dao_coin_blind, ]); @@ -123,6 +121,7 @@ impl Builder { let zk_bin = zk_info.bincode.clone(); let prover_witnesses = vec![ + // // proposal params Witness::Base(Value::known(*proposal_dest_coords.x())), Witness::Base(Value::known(*proposal_dest_coords.y())), @@ -173,7 +172,6 @@ impl Builder { ]; let circuit = ZkCircuit::new(prover_witnesses, zk_bin); - debug!(target: "example_contract::foo::wallet::Builder", "input_proof Proof::create()"); let proving_key = &zk_info.proving_key; let input_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) .expect("DAO::exec() proving error!)"); diff --git a/example/dao/contract/dao_contract/mint/mod.rs b/example/dao/src/contract/dao/mint/mod.rs similarity index 100% rename from example/dao/contract/dao_contract/mint/mod.rs rename to example/dao/src/contract/dao/mint/mod.rs diff --git a/example/dao/contract/dao_contract/mint/validate.rs b/example/dao/src/contract/dao/mint/validate.rs similarity index 90% rename from example/dao/contract/dao_contract/mint/validate.rs rename to example/dao/src/contract/dao/mint/validate.rs index 35a29c3b4..ddd13dcda 100644 --- a/example/dao/contract/dao_contract/mint/validate.rs +++ b/example/dao/src/contract/dao/mint/validate.rs @@ -4,7 +4,7 @@ use darkfi::crypto::{keypair::PublicKey, types::DrkCircuitField}; use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; use crate::{ - contract::dao_contract::{DaoBulla, State, CONTRACT_ID}, + contract::dao::{DaoBulla, State, CONTRACT_ID}, util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, }; @@ -16,7 +16,7 @@ pub fn state_transition( let func_call = &parent_tx.func_calls[func_call_index]; let call_data = func_call.call_data.as_any(); - assert_eq!((*call_data).type_id(), TypeId::of::()); + assert_eq!((&*call_data).type_id(), TypeId::of::()); let call_data = call_data.downcast_ref::(); // This will be inside wasm so unwrap is fine. @@ -65,7 +65,7 @@ impl CallDataBase for CallData { fn encode_bytes( &self, mut writer: &mut dyn std::io::Write, - ) -> core::result::Result { + ) -> std::result::Result { self.encode(&mut writer) } } diff --git a/example/dao/contract/dao_contract/mint/wallet.rs b/example/dao/src/contract/dao/mint/wallet.rs similarity index 97% rename from example/dao/contract/dao_contract/mint/wallet.rs rename to example/dao/src/contract/dao/mint/wallet.rs index cf4b4bc4f..9882fd06f 100644 --- a/example/dao/contract/dao_contract/mint/wallet.rs +++ b/example/dao/src/contract/dao/mint/wallet.rs @@ -1,3 +1,7 @@ +use halo2_proofs::circuit::Value; +use pasta_curves::{arithmetic::CurveAffine, group::Curve, pallas}; +use rand::rngs::OsRng; + use darkfi::{ crypto::{ keypair::{PublicKey, SecretKey}, @@ -6,12 +10,9 @@ use darkfi::{ }, zk::vm::{Witness, ZkCircuit}, }; -use halo2_proofs::circuit::Value; -use pasta_curves::{arithmetic::CurveAffine, group::Curve, pallas}; -use rand::rngs::OsRng; use crate::{ - contract::dao_contract::{mint::validate::CallData, state::DaoBulla, CONTRACT_ID}, + contract::dao::{mint::validate::CallData, state::DaoBulla, CONTRACT_ID}, util::{FuncCall, ZkContractInfo, ZkContractTable}, }; diff --git a/example/dao/contract/dao_contract/mod.rs b/example/dao/src/contract/dao/mod.rs similarity index 100% rename from example/dao/contract/dao_contract/mod.rs rename to example/dao/src/contract/dao/mod.rs diff --git a/example/dao/contract/dao_contract/propose/mod.rs b/example/dao/src/contract/dao/propose/mod.rs similarity index 100% rename from example/dao/contract/dao_contract/propose/mod.rs rename to example/dao/src/contract/dao/propose/mod.rs diff --git a/example/dao/src/contract/dao/propose/validate.rs b/example/dao/src/contract/dao/propose/validate.rs new file mode 100644 index 000000000..12d3bd375 --- /dev/null +++ b/example/dao/src/contract/dao/propose/validate.rs @@ -0,0 +1,170 @@ +use std::any::{Any, TypeId}; + +use log::error; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{Curve, Group}, + pallas, +}; + +use darkfi::{ + crypto::{keypair::PublicKey, merkle_node::MerkleNode, types::DrkCircuitField}, + Error as DarkFiError, +}; +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{dao, dao::State as DaoState, money, money::state::State as MoneyState}, + note::EncryptedNote2, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, +}; + +// used for debugging +// const TARGET: &str = "dao_contract::propose::validate::state_transition()"; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("Invalid input merkle root")] + InvalidInputMerkleRoot, + + #[error("Invalid DAO merkle root")] + InvalidDaoMerkleRoot, + + #[error("DarkFi error: {0}")] + DarkFiError(String), +} +type Result = std::result::Result; + +impl From for Error { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct CallData { + pub header: Header, + pub inputs: Vec, +} + +impl CallDataBase for CallData { + fn zk_public_values(&self) -> Vec<(String, Vec)> { + let mut zk_publics = Vec::new(); + let mut total_funds_commit = pallas::Point::identity(); + + assert!(self.inputs.len() > 0, "inputs length cannot be zero"); + for input in &self.inputs { + total_funds_commit += input.value_commit; + let value_coords = input.value_commit.to_affine().coordinates().unwrap(); + + let sigpub_coords = input.signature_public.0.to_affine().coordinates().unwrap(); + + zk_publics.push(( + "dao-propose-burn".to_string(), + vec![ + *value_coords.x(), + *value_coords.y(), + self.header.token_commit, + input.merkle_root.0, + *sigpub_coords.x(), + *sigpub_coords.y(), + ], + )); + } + + let total_funds_coords = total_funds_commit.to_affine().coordinates().unwrap(); + zk_publics.push(( + "dao-propose-main".to_string(), + vec![ + self.header.token_commit, + self.header.dao_merkle_root.0, + self.header.proposal_bulla, + *total_funds_coords.x(), + *total_funds_coords.y(), + ], + )); + + zk_publics + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn signature_public_keys(&self) -> Vec { + let mut signature_public_keys = vec![]; + for input in self.inputs.clone() { + signature_public_keys.push(input.signature_public); + } + signature_public_keys + } + + fn encode_bytes( + &self, + mut writer: &mut dyn std::io::Write, + ) -> std::result::Result { + self.encode(&mut writer) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Header { + pub dao_merkle_root: MerkleNode, + pub token_commit: pallas::Base, + pub proposal_bulla: pallas::Base, + pub enc_note: EncryptedNote2, +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Input { + pub value_commit: pallas::Point, + pub merkle_root: MerkleNode, + pub signature_public: PublicKey, +} + +pub fn state_transition( + states: &StateRegistry, + func_call_index: usize, + parent_tx: &Transaction, +) -> Result> { + let func_call = &parent_tx.func_calls[func_call_index]; + let call_data = func_call.call_data.as_any(); + + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::(); + + // This will be inside wasm so unwrap is fine. + let call_data = call_data.unwrap(); + + // Check the merkle roots for the input coins are valid + for input in &call_data.inputs { + let money_state = states.lookup::(*money::CONTRACT_ID).unwrap(); + if !money_state.is_valid_merkle(&input.merkle_root) { + return Err(Error::InvalidInputMerkleRoot) + } + } + + let state = states.lookup::(*dao::CONTRACT_ID).unwrap(); + + // Is the DAO bulla generated in the ZK proof valid + if !state.is_valid_dao_merkle(&call_data.header.dao_merkle_root) { + return Err(Error::InvalidDaoMerkleRoot) + } + + // TODO: look at gov tokens avoid using already spent ones + // Need to spend original coin and generate 2 nullifiers? + + Ok(Box::new(Update { proposal_bulla: call_data.header.proposal_bulla })) +} + +#[derive(Clone)] +pub struct Update { + pub proposal_bulla: pallas::Base, +} + +impl UpdateBase for Update { + fn apply(self: Box, states: &mut StateRegistry) { + let state = states.lookup_mut::(*dao::CONTRACT_ID).unwrap(); + state.add_proposal_bulla(self.proposal_bulla); + } +} diff --git a/example/dao/src/contract/dao/propose/wallet.rs b/example/dao/src/contract/dao/propose/wallet.rs new file mode 100644 index 000000000..0b637a6e8 --- /dev/null +++ b/example/dao/src/contract/dao/propose/wallet.rs @@ -0,0 +1,272 @@ +use halo2_proofs::circuit::Value; +use incrementalmerkletree::Hashable; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{ff::Field, Curve}, + pallas, +}; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + keypair::{PublicKey, SecretKey}, + merkle_node::MerkleNode, + util::{pedersen_commitment_u64, poseidon_hash}, + Proof, + }, + zk::vm::{Witness, ZkCircuit}, +}; +use darkfi_serial::{SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{ + dao::{ + mint::wallet::DaoParams, + propose::validate::{CallData, Header, Input}, + CONTRACT_ID, + }, + money, + }, + note, + util::{FuncCall, ZkContractInfo, ZkContractTable}, +}; + +#[derive(SerialEncodable, SerialDecodable)] +pub struct Note { + pub proposal: Proposal, +} + +pub struct BuilderInput { + pub secret: SecretKey, + pub note: money::transfer::wallet::Note, + pub leaf_position: incrementalmerkletree::Position, + pub merkle_path: Vec, + pub signature_secret: SecretKey, +} + +#[derive(SerialEncodable, SerialDecodable, Clone)] +pub struct Proposal { + pub dest: PublicKey, + pub amount: u64, + pub serial: pallas::Base, + pub token_id: pallas::Base, + pub blind: pallas::Base, +} + +pub struct Builder { + pub inputs: Vec, + pub proposal: Proposal, + pub dao: DaoParams, + pub dao_leaf_position: incrementalmerkletree::Position, + pub dao_merkle_path: Vec, + pub dao_merkle_root: MerkleNode, +} + +impl Builder { + pub fn build(self, zk_bins: &ZkContractTable) -> FuncCall { + let mut proofs = vec![]; + + let gov_token_blind = pallas::Base::random(&mut OsRng); + + let mut inputs = vec![]; + let mut total_funds = 0; + let mut total_funds_blinds = pallas::Scalar::from(0); + + for input in self.inputs { + let funds_blind = pallas::Scalar::random(&mut OsRng); + total_funds += input.note.value; + total_funds_blinds += funds_blind; + + let signature_public = PublicKey::from_secret(input.signature_secret); + + let zk_info = zk_bins.lookup(&"dao-propose-burn".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + let zk_bin = zk_info.bincode.clone(); + + // Note from the previous output + let note = input.note; + let leaf_pos: u64 = input.leaf_position.into(); + + let prover_witnesses = vec![ + Witness::Base(Value::known(input.secret.0)), + Witness::Base(Value::known(note.serial)), + Witness::Base(Value::known(pallas::Base::from(0))), + Witness::Base(Value::known(pallas::Base::from(0))), + Witness::Base(Value::known(pallas::Base::from(note.value))), + Witness::Base(Value::known(note.token_id)), + Witness::Base(Value::known(note.coin_blind)), + Witness::Scalar(Value::known(funds_blind)), + Witness::Base(Value::known(gov_token_blind)), + Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())), + Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())), + Witness::Base(Value::known(input.signature_secret.0)), + ]; + + let public_key = PublicKey::from_secret(input.secret); + let coords = public_key.0.to_affine().coordinates().unwrap(); + + let coin = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + pallas::Base::from(note.value), + note.token_id, + note.serial, + pallas::Base::from(0), + pallas::Base::from(0), + note.coin_blind, + ]); + + let merkle_root = { + let position: u64 = input.leaf_position.into(); + let mut current = MerkleNode(coin); + for (level, sibling) in input.merkle_path.iter().enumerate() { + let level = level as u8; + current = if position & (1 << level) == 0 { + MerkleNode::combine(level.into(), ¤t, sibling) + } else { + MerkleNode::combine(level.into(), sibling, ¤t) + }; + } + current + }; + + let token_commit = poseidon_hash::<2>([note.token_id, gov_token_blind]); + assert_eq!(self.dao.gov_token_id, note.token_id); + + let value_commit = pedersen_commitment_u64(note.value, funds_blind); + let value_coords = value_commit.to_affine().coordinates().unwrap(); + + let sigpub_coords = signature_public.0.to_affine().coordinates().unwrap(); + + let public_inputs = vec![ + *value_coords.x(), + *value_coords.y(), + token_commit, + merkle_root.0, + *sigpub_coords.x(), + *sigpub_coords.y(), + ]; + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + + let proving_key = &zk_info.proving_key; + let input_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::propose() proving error!"); + proofs.push(input_proof); + + let input = Input { value_commit, merkle_root, signature_public }; + inputs.push(input); + } + + let total_funds_commit = pedersen_commitment_u64(total_funds, total_funds_blinds); + let total_funds_coords = total_funds_commit.to_affine().coordinates().unwrap(); + let total_funds = pallas::Base::from(total_funds); + + let token_commit = poseidon_hash::<2>([self.dao.gov_token_id, gov_token_blind]); + + let proposal_dest_coords = self.proposal.dest.0.to_affine().coordinates().unwrap(); + let proposal_dest_x = *proposal_dest_coords.x(); + let proposal_dest_y = *proposal_dest_coords.y(); + + let proposal_amount = pallas::Base::from(self.proposal.amount); + + let dao_proposer_limit = pallas::Base::from(self.dao.proposer_limit); + let dao_quorum = pallas::Base::from(self.dao.quorum); + let dao_approval_ratio_quot = pallas::Base::from(self.dao.approval_ratio_quot); + let dao_approval_ratio_base = pallas::Base::from(self.dao.approval_ratio_base); + + let dao_pubkey_coords = self.dao.public_key.0.to_affine().coordinates().unwrap(); + + let dao_bulla = poseidon_hash::<8>([ + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + self.dao.gov_token_id, + *dao_pubkey_coords.x(), + *dao_pubkey_coords.y(), + self.dao.bulla_blind, + ]); + + let dao_leaf_position: u64 = self.dao_leaf_position.into(); + + let proposal_bulla = poseidon_hash::<8>([ + proposal_dest_x, + proposal_dest_y, + proposal_amount, + self.proposal.serial, + self.proposal.token_id, + dao_bulla, + self.proposal.blind, + // @tmp-workaround + self.proposal.blind, + ]); + + let zk_info = zk_bins.lookup(&"dao-propose-main".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + let zk_bin = zk_info.bincode.clone(); + let prover_witnesses = vec![ + // Proposers total number of gov tokens + Witness::Base(Value::known(total_funds)), + Witness::Scalar(Value::known(total_funds_blinds)), + // Used for blinding exported gov token ID + Witness::Base(Value::known(gov_token_blind)), + // proposal params + Witness::Base(Value::known(proposal_dest_x)), + Witness::Base(Value::known(proposal_dest_y)), + Witness::Base(Value::known(proposal_amount)), + Witness::Base(Value::known(self.proposal.serial)), + Witness::Base(Value::known(self.proposal.token_id)), + Witness::Base(Value::known(self.proposal.blind)), + // DAO params + Witness::Base(Value::known(dao_proposer_limit)), + Witness::Base(Value::known(dao_quorum)), + Witness::Base(Value::known(dao_approval_ratio_quot)), + Witness::Base(Value::known(dao_approval_ratio_base)), + Witness::Base(Value::known(self.dao.gov_token_id)), + Witness::Base(Value::known(*dao_pubkey_coords.x())), + Witness::Base(Value::known(*dao_pubkey_coords.y())), + Witness::Base(Value::known(self.dao.bulla_blind)), + Witness::Uint32(Value::known(dao_leaf_position.try_into().unwrap())), + Witness::MerklePath(Value::known(self.dao_merkle_path.try_into().unwrap())), + ]; + let public_inputs = vec![ + token_commit, + self.dao_merkle_root.0, + proposal_bulla, + *total_funds_coords.x(), + *total_funds_coords.y(), + ]; + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + + let proving_key = &zk_info.proving_key; + let main_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::propose() proving error!"); + proofs.push(main_proof); + + let note = Note { proposal: self.proposal }; + let enc_note = note::encrypt(¬e, &self.dao.public_key).unwrap(); + let header = Header { + dao_merkle_root: self.dao_merkle_root, + proposal_bulla, + token_commit, + enc_note, + }; + + let call_data = CallData { header, inputs }; + + FuncCall { + contract_id: *CONTRACT_ID, + func_id: *super::FUNC_ID, + call_data: Box::new(call_data), + proofs, + } + } +} diff --git a/example/dao/src/contract/dao/state.rs b/example/dao/src/contract/dao/state.rs new file mode 100644 index 000000000..eb98b0e07 --- /dev/null +++ b/example/dao/src/contract/dao/state.rs @@ -0,0 +1,97 @@ +use std::{any::Any, collections::HashMap}; + +use incrementalmerkletree::{bridgetree::BridgeTree, Tree}; +use pasta_curves::{group::Group, pallas}; + +use darkfi::crypto::{constants::MERKLE_DEPTH, merkle_node::MerkleNode, nullifier::Nullifier}; +use darkfi_serial::{SerialDecodable, SerialEncodable}; + +use crate::util::HashableBase; + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct DaoBulla(pub pallas::Base); + +type MerkleTree = BridgeTree; + +pub struct ProposalVotes { + // TODO: might be more logical to have 'yes_votes_commit' and 'no_votes_commit' + /// Weighted vote commit + pub yes_votes_commit: pallas::Point, + /// All value staked in the vote + pub all_votes_commit: pallas::Point, + /// Vote nullifiers + pub vote_nulls: Vec, +} + +impl ProposalVotes { + pub fn nullifier_exists(&self, nullifier: &Nullifier) -> bool { + self.vote_nulls.iter().any(|n| n == nullifier) + } +} + +/// This DAO state is for all DAOs on the network. There should only be a single instance. +pub struct State { + dao_bullas: Vec, + pub dao_tree: MerkleTree, + pub dao_roots: Vec, + + //proposal_bullas: Vec, + pub proposal_tree: MerkleTree, + pub proposal_roots: Vec, + pub proposal_votes: HashMap, +} + +impl State { + pub fn new() -> Box { + Box::new(Self { + dao_bullas: Vec::new(), + dao_tree: MerkleTree::new(100), + dao_roots: Vec::new(), + //proposal_bullas: Vec::new(), + proposal_tree: MerkleTree::new(100), + proposal_roots: Vec::new(), + proposal_votes: HashMap::new(), + }) + } + + pub fn add_dao_bulla(&mut self, bulla: DaoBulla) { + let node = MerkleNode(bulla.0); + self.dao_bullas.push(bulla); + self.dao_tree.append(&node); + self.dao_roots.push(self.dao_tree.root(0).unwrap()); + } + + pub fn add_proposal_bulla(&mut self, bulla: pallas::Base) { + let node = MerkleNode(bulla); + //self.proposal_bullas.push(bulla); + self.proposal_tree.append(&node); + self.proposal_roots.push(self.proposal_tree.root(0).unwrap()); + self.proposal_votes.insert( + HashableBase(bulla), + ProposalVotes { + yes_votes_commit: pallas::Point::identity(), + all_votes_commit: pallas::Point::identity(), + vote_nulls: Vec::new(), + }, + ); + } + + pub fn lookup_proposal_votes(&self, proposal_bulla: pallas::Base) -> Option<&ProposalVotes> { + self.proposal_votes.get(&HashableBase(proposal_bulla)) + } + pub fn lookup_proposal_votes_mut( + &mut self, + proposal_bulla: pallas::Base, + ) -> Option<&mut ProposalVotes> { + self.proposal_votes.get_mut(&HashableBase(proposal_bulla)) + } + + pub fn is_valid_dao_merkle(&self, root: &MerkleNode) -> bool { + self.dao_roots.iter().any(|m| m == root) + } + + // TODO: This never gets called. + pub fn _is_valid_proposal_merkle(&self, root: &MerkleNode) -> bool { + self.proposal_roots.iter().any(|m| m == root) + } +} diff --git a/example/dao/contract/dao_contract/vote/mod.rs b/example/dao/src/contract/dao/vote/mod.rs similarity index 100% rename from example/dao/contract/dao_contract/vote/mod.rs rename to example/dao/src/contract/dao/vote/mod.rs diff --git a/example/dao/src/contract/dao/vote/validate.rs b/example/dao/src/contract/dao/vote/validate.rs new file mode 100644 index 000000000..29132d6ce --- /dev/null +++ b/example/dao/src/contract/dao/vote/validate.rs @@ -0,0 +1,205 @@ +use std::any::{Any, TypeId}; + +use log::error; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{Curve, Group}, + pallas, +}; + +use darkfi::{ + crypto::{ + keypair::PublicKey, merkle_node::MerkleNode, nullifier::Nullifier, types::DrkCircuitField, + }, + Error as DarkFiError, +}; +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{dao, dao::State as DaoState, money, money::state::State as MoneyState}, + note::EncryptedNote2, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, +}; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("Invalid proposal")] + InvalidProposal, + + #[error("Voting with already spent coinage")] + SpentCoin, + + #[error("Double voting")] + DoubleVote, + + #[error("Invalid input merkle root")] + InvalidInputMerkleRoot, + + #[error("DarkFi error: {0}")] + DarkFiError(String), +} +type Result = std::result::Result; + +impl From for Error { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct CallData { + pub header: Header, + pub inputs: Vec, +} + +impl CallDataBase for CallData { + fn zk_public_values(&self) -> Vec<(String, Vec)> { + let mut zk_publics = Vec::new(); + let mut all_votes_commit = pallas::Point::identity(); + + assert!(self.inputs.len() > 0, "inputs length cannot be zero"); + for input in &self.inputs { + all_votes_commit += input.vote_commit; + let value_coords = input.vote_commit.to_affine().coordinates().unwrap(); + + let sigpub_coords = input.signature_public.0.to_affine().coordinates().unwrap(); + + zk_publics.push(( + "dao-vote-burn".to_string(), + vec![ + input.nullifier.0, + *value_coords.x(), + *value_coords.y(), + self.header.token_commit, + input.merkle_root.0, + *sigpub_coords.x(), + *sigpub_coords.y(), + ], + )); + } + + let yes_vote_commit_coords = self.header.yes_vote_commit.to_affine().coordinates().unwrap(); + + let vote_commit_coords = all_votes_commit.to_affine().coordinates().unwrap(); + + zk_publics.push(( + "dao-vote-main".to_string(), + vec![ + self.header.token_commit, + self.header.proposal_bulla, + *yes_vote_commit_coords.x(), + *yes_vote_commit_coords.y(), + *vote_commit_coords.x(), + *vote_commit_coords.y(), + ], + )); + + zk_publics + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn signature_public_keys(&self) -> Vec { + let mut signature_public_keys = vec![]; + for input in self.inputs.clone() { + signature_public_keys.push(input.signature_public); + } + signature_public_keys + } + + fn encode_bytes( + &self, + mut writer: &mut dyn std::io::Write, + ) -> std::result::Result { + self.encode(&mut writer) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Header { + pub token_commit: pallas::Base, + pub proposal_bulla: pallas::Base, + pub yes_vote_commit: pallas::Point, + pub enc_note: EncryptedNote2, +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Input { + pub nullifier: Nullifier, + pub vote_commit: pallas::Point, + pub merkle_root: MerkleNode, + pub signature_public: PublicKey, +} + +pub fn state_transition( + states: &StateRegistry, + func_call_index: usize, + parent_tx: &Transaction, +) -> Result> { + let func_call = &parent_tx.func_calls[func_call_index]; + let call_data = func_call.call_data.as_any(); + + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::(); + + // This will be inside wasm so unwrap is fine. + let call_data = call_data.unwrap(); + + let dao_state = states.lookup::(*dao::CONTRACT_ID).unwrap(); + + // Check proposal_bulla exists + let votes_info = dao_state.lookup_proposal_votes(call_data.header.proposal_bulla); + if votes_info.is_none() { + return Err(Error::InvalidProposal) + } + let votes_info = votes_info.unwrap(); + + // Check the merkle roots for the input coins are valid + let mut vote_nulls = Vec::new(); + let mut all_vote_commit = pallas::Point::identity(); + for input in &call_data.inputs { + let money_state = states.lookup::(*money::CONTRACT_ID).unwrap(); + if !money_state.is_valid_merkle(&input.merkle_root) { + return Err(Error::InvalidInputMerkleRoot) + } + + if money_state.nullifier_exists(&input.nullifier) { + return Err(Error::SpentCoin) + } + + if votes_info.nullifier_exists(&input.nullifier) { + return Err(Error::DoubleVote) + } + + all_vote_commit += input.vote_commit; + + vote_nulls.push(input.nullifier); + } + + Ok(Box::new(Update { + proposal_bulla: call_data.header.proposal_bulla, + vote_nulls, + yes_vote_commit: call_data.header.yes_vote_commit, + all_vote_commit, + })) +} + +#[derive(Clone)] +pub struct Update { + proposal_bulla: pallas::Base, + vote_nulls: Vec, + pub yes_vote_commit: pallas::Point, + pub all_vote_commit: pallas::Point, +} + +impl UpdateBase for Update { + fn apply(mut self: Box, states: &mut StateRegistry) { + let state = states.lookup_mut::(*dao::CONTRACT_ID).unwrap(); + let votes_info = state.lookup_proposal_votes_mut(self.proposal_bulla).unwrap(); + votes_info.yes_votes_commit += self.yes_vote_commit; + votes_info.all_votes_commit += self.all_vote_commit; + votes_info.vote_nulls.append(&mut self.vote_nulls); + } +} diff --git a/example/dao/src/contract/dao/vote/wallet.rs b/example/dao/src/contract/dao/vote/wallet.rs new file mode 100644 index 000000000..75f24944b --- /dev/null +++ b/example/dao/src/contract/dao/vote/wallet.rs @@ -0,0 +1,292 @@ +use halo2_proofs::circuit::Value; +use incrementalmerkletree::Hashable; +use log::debug; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{ff::Field, Curve}, + pallas, +}; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + keypair::{Keypair, PublicKey, SecretKey}, + merkle_node::MerkleNode, + nullifier::Nullifier, + util::{pedersen_commitment_u64, poseidon_hash}, + Proof, + }, + zk::vm::{Witness, ZkCircuit}, +}; +use darkfi_serial::{SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{ + dao::{ + mint::wallet::DaoParams, + propose::wallet::Proposal, + vote::validate::{CallData, Header, Input}, + CONTRACT_ID, + }, + money, + }, + note, + util::{FuncCall, ZkContractInfo, ZkContractTable}, +}; + +#[derive(SerialEncodable, SerialDecodable)] +pub struct Note { + pub vote: Vote, + pub vote_value: u64, + pub vote_value_blind: pallas::Scalar, +} + +#[derive(SerialEncodable, SerialDecodable)] +pub struct Vote { + pub vote_option: bool, + pub vote_option_blind: pallas::Scalar, +} + +pub struct BuilderInput { + pub secret: SecretKey, + pub note: money::transfer::wallet::Note, + pub leaf_position: incrementalmerkletree::Position, + pub merkle_path: Vec, + pub signature_secret: SecretKey, +} + +// TODO: should be token locking voting? +// Inside ZKproof, check proposal is correct. +pub struct Builder { + pub inputs: Vec, + pub vote: Vote, + pub vote_keypair: Keypair, + pub proposal: Proposal, + pub dao: DaoParams, +} + +impl Builder { + pub fn build(self, zk_bins: &ZkContractTable) -> FuncCall { + debug!(target: "dao_contract::vote::wallet::Builder", "build()"); + let mut proofs = vec![]; + + let gov_token_blind = pallas::Base::random(&mut OsRng); + + let mut inputs = vec![]; + let mut vote_value = 0; + let mut vote_value_blind = pallas::Scalar::from(0); + + for input in self.inputs { + let value_blind = pallas::Scalar::random(&mut OsRng); + + vote_value += input.note.value; + vote_value_blind += value_blind; + + let signature_public = PublicKey::from_secret(input.signature_secret); + + let zk_info = zk_bins.lookup(&"dao-vote-burn".to_string()).unwrap(); + + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + let zk_bin = zk_info.bincode.clone(); + + // Note from the previous output + let note = input.note; + let leaf_pos: u64 = input.leaf_position.into(); + + let prover_witnesses = vec![ + Witness::Base(Value::known(input.secret.0)), + Witness::Base(Value::known(note.serial)), + Witness::Base(Value::known(pallas::Base::from(0))), + Witness::Base(Value::known(pallas::Base::from(0))), + Witness::Base(Value::known(pallas::Base::from(note.value))), + Witness::Base(Value::known(note.token_id)), + Witness::Base(Value::known(note.coin_blind)), + Witness::Scalar(Value::known(vote_value_blind)), + Witness::Base(Value::known(gov_token_blind)), + Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())), + Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())), + Witness::Base(Value::known(input.signature_secret.0)), + ]; + + let public_key = PublicKey::from_secret(input.secret); + let coords = public_key.0.to_affine().coordinates().unwrap(); + + let coin = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + pallas::Base::from(note.value), + note.token_id, + note.serial, + pallas::Base::from(0), + pallas::Base::from(0), + note.coin_blind, + ]); + + let merkle_root = { + let position: u64 = input.leaf_position.into(); + let mut current = MerkleNode(coin); + for (level, sibling) in input.merkle_path.iter().enumerate() { + let level = level as u8; + current = if position & (1 << level) == 0 { + MerkleNode::combine(level.into(), ¤t, sibling) + } else { + MerkleNode::combine(level.into(), sibling, ¤t) + }; + } + current + }; + + let token_commit = poseidon_hash::<2>([note.token_id, gov_token_blind]); + assert_eq!(self.dao.gov_token_id, note.token_id); + + let nullifier = poseidon_hash::<2>([input.secret.0, note.serial]); + + let vote_commit = pedersen_commitment_u64(note.value, vote_value_blind); + let vote_commit_coords = vote_commit.to_affine().coordinates().unwrap(); + + let sigpub_coords = signature_public.0.to_affine().coordinates().unwrap(); + + let public_inputs = vec![ + nullifier, + *vote_commit_coords.x(), + *vote_commit_coords.y(), + token_commit, + merkle_root.0, + *sigpub_coords.x(), + *sigpub_coords.y(), + ]; + + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + let proving_key = &zk_info.proving_key; + debug!(target: "dao_contract::vote::wallet::Builder", "input_proof Proof::create()"); + let input_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::vote() proving error!"); + proofs.push(input_proof); + + let input = Input { + nullifier: Nullifier(nullifier), + vote_commit, + merkle_root, + signature_public, + }; + inputs.push(input); + } + + let token_commit = poseidon_hash::<2>([self.dao.gov_token_id, gov_token_blind]); + + let proposal_dest_coords = self.proposal.dest.0.to_affine().coordinates().unwrap(); + + let proposal_amount = pallas::Base::from(self.proposal.amount); + + let dao_proposer_limit = pallas::Base::from(self.dao.proposer_limit); + let dao_quorum = pallas::Base::from(self.dao.quorum); + let dao_approval_ratio_quot = pallas::Base::from(self.dao.approval_ratio_quot); + let dao_approval_ratio_base = pallas::Base::from(self.dao.approval_ratio_base); + + let dao_pubkey_coords = self.dao.public_key.0.to_affine().coordinates().unwrap(); + + let dao_bulla = poseidon_hash::<8>([ + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + self.dao.gov_token_id, + *dao_pubkey_coords.x(), + *dao_pubkey_coords.y(), + self.dao.bulla_blind, + ]); + + let proposal_bulla = poseidon_hash::<8>([ + *proposal_dest_coords.x(), + *proposal_dest_coords.y(), + proposal_amount, + self.proposal.serial, + self.proposal.token_id, + dao_bulla, + self.proposal.blind, + // @tmp-workaround + self.proposal.blind, + ]); + + let vote_option = self.vote.vote_option as u64; + assert!(vote_option == 0 || vote_option == 1); + + let yes_vote_commit = + pedersen_commitment_u64(vote_option * vote_value, self.vote.vote_option_blind); + let yes_vote_commit_coords = yes_vote_commit.to_affine().coordinates().unwrap(); + + let all_vote_commit = pedersen_commitment_u64(vote_value, vote_value_blind); + let all_vote_commit_coords = all_vote_commit.to_affine().coordinates().unwrap(); + + let zk_info = zk_bins.lookup(&"dao-vote-main".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + let zk_bin = zk_info.bincode.clone(); + + let prover_witnesses = vec![ + // proposal params + Witness::Base(Value::known(*proposal_dest_coords.x())), + Witness::Base(Value::known(*proposal_dest_coords.y())), + Witness::Base(Value::known(proposal_amount)), + Witness::Base(Value::known(self.proposal.serial)), + Witness::Base(Value::known(self.proposal.token_id)), + Witness::Base(Value::known(self.proposal.blind)), + // DAO params + Witness::Base(Value::known(dao_proposer_limit)), + Witness::Base(Value::known(dao_quorum)), + Witness::Base(Value::known(dao_approval_ratio_quot)), + Witness::Base(Value::known(dao_approval_ratio_base)), + Witness::Base(Value::known(self.dao.gov_token_id)), + Witness::Base(Value::known(*dao_pubkey_coords.x())), + Witness::Base(Value::known(*dao_pubkey_coords.y())), + Witness::Base(Value::known(self.dao.bulla_blind)), + // Vote + Witness::Base(Value::known(pallas::Base::from(vote_option))), + Witness::Scalar(Value::known(self.vote.vote_option_blind)), + // Total number of gov tokens allocated + Witness::Base(Value::known(pallas::Base::from(vote_value))), + Witness::Scalar(Value::known(vote_value_blind)), + // gov token + Witness::Base(Value::known(gov_token_blind)), + ]; + + let public_inputs = vec![ + token_commit, + proposal_bulla, + // this should be a value commit?? + *yes_vote_commit_coords.x(), + *yes_vote_commit_coords.y(), + *all_vote_commit_coords.x(), + *all_vote_commit_coords.y(), + ]; + + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + + let proving_key = &zk_info.proving_key; + debug!(target: "dao_contract::vote::wallet::Builder", "main_proof = Proof::create()"); + let main_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::vote() proving error!"); + proofs.push(main_proof); + + let note = Note { vote: self.vote, vote_value, vote_value_blind }; + let enc_note = note::encrypt(¬e, &self.vote_keypair.public).unwrap(); + + let header = Header { token_commit, proposal_bulla, yes_vote_commit, enc_note }; + + let call_data = CallData { header, inputs }; + + FuncCall { + contract_id: *CONTRACT_ID, + func_id: *super::FUNC_ID, + call_data: Box::new(call_data), + proofs, + } + } +} diff --git a/example/dao/contract/example_contract/foo/mod.rs b/example/dao/src/contract/example/foo/mod.rs similarity index 100% rename from example/dao/contract/example_contract/foo/mod.rs rename to example/dao/src/contract/example/foo/mod.rs diff --git a/example/dao/contract/example_contract/foo/validate.rs b/example/dao/src/contract/example/foo/validate.rs similarity index 92% rename from example/dao/contract/example_contract/foo/validate.rs rename to example/dao/src/contract/example/foo/validate.rs index 5a21618d0..6d445856d 100644 --- a/example/dao/contract/example_contract/foo/validate.rs +++ b/example/dao/src/contract/example/foo/validate.rs @@ -1,15 +1,15 @@ -use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; +use std::any::{Any, TypeId}; + use pasta_curves::pallas; use darkfi::{ crypto::{keypair::PublicKey, types::DrkCircuitField}, Error as DarkFiError, }; - -use std::any::{Any, TypeId}; +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; use crate::{ - contract::example_contract::{state::State, CONTRACT_ID}, + contract::example::{state::State, CONTRACT_ID}, util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, }; @@ -19,7 +19,6 @@ type Result = std::result::Result; pub enum Error { #[error("ValueExists")] ValueExists, - #[error("DarkFi error: {0}")] DarkFiError(String), } @@ -52,7 +51,7 @@ impl CallDataBase for CallData { fn encode_bytes( &self, mut writer: &mut dyn std::io::Write, - ) -> core::result::Result { + ) -> std::result::Result { self.encode(&mut writer) } } @@ -65,7 +64,7 @@ pub fn state_transition( let func_call = &parent_tx.func_calls[func_call_index]; let call_data = func_call.call_data.as_any(); - assert_eq!((*call_data).type_id(), TypeId::of::()); + assert_eq!((&*call_data).type_id(), TypeId::of::()); let call_data = call_data.downcast_ref::(); // This will be inside wasm so unwrap is fine. diff --git a/example/dao/contract/example_contract/foo/wallet.rs b/example/dao/src/contract/example/foo/wallet.rs similarity index 96% rename from example/dao/contract/example_contract/foo/wallet.rs rename to example/dao/src/contract/example/foo/wallet.rs index c41d9d5fa..719b2e05b 100644 --- a/example/dao/contract/example_contract/foo/wallet.rs +++ b/example/dao/src/contract/example/foo/wallet.rs @@ -13,7 +13,7 @@ use darkfi::{ }; use crate::{ - contract::example_contract::{foo::validate::CallData, CONTRACT_ID}, + contract::example::{foo::validate::CallData, CONTRACT_ID}, util::{FuncCall, ZkContractInfo, ZkContractTable}, }; diff --git a/example/dao/contract/example_contract/mod.rs b/example/dao/src/contract/example/mod.rs similarity index 100% rename from example/dao/contract/example_contract/mod.rs rename to example/dao/src/contract/example/mod.rs diff --git a/example/dao/contract/example_contract/state.rs b/example/dao/src/contract/example/state.rs similarity index 90% rename from example/dao/contract/example_contract/state.rs rename to example/dao/src/contract/example/state.rs index dc9d8c3d3..d7b6218ff 100644 --- a/example/dao/contract/example_contract/state.rs +++ b/example/dao/src/contract/example/state.rs @@ -7,14 +7,14 @@ pub struct State { } impl State { - pub fn new() -> Box { + pub fn new() -> Box { Box::new(Self { public_values: Vec::new() }) } pub fn add_public_value(&mut self, public_value: pallas::Base) { self.public_values.push(public_value) } - + // pub fn public_exists(&self, public_value: &pallas::Base) -> bool { self.public_values.iter().any(|v| v == public_value) } diff --git a/example/dao/src/contract/mod.rs b/example/dao/src/contract/mod.rs new file mode 100644 index 000000000..bb4d195c3 --- /dev/null +++ b/example/dao/src/contract/mod.rs @@ -0,0 +1,3 @@ +pub mod dao; +pub mod example; +pub mod money; diff --git a/example/dao/contract/money_contract/mod.rs b/example/dao/src/contract/money/mod.rs similarity index 100% rename from example/dao/contract/money_contract/mod.rs rename to example/dao/src/contract/money/mod.rs diff --git a/example/dao/src/contract/money/state.rs b/example/dao/src/contract/money/state.rs new file mode 100644 index 000000000..dbc26a7e4 --- /dev/null +++ b/example/dao/src/contract/money/state.rs @@ -0,0 +1,116 @@ +use incrementalmerkletree::{bridgetree::BridgeTree, Tree}; + +use darkfi::crypto::{ + coin::Coin, + constants::MERKLE_DEPTH, + keypair::{PublicKey, SecretKey}, + merkle_node::MerkleNode, + nullifier::Nullifier, +}; + +use super::transfer; +use crate::note::EncryptedNote2; + +type MerkleTree = BridgeTree; + +pub struct OwnCoin { + pub coin: Coin, + pub note: transfer::wallet::Note, + pub leaf_position: incrementalmerkletree::Position, +} + +pub struct WalletCache { + // Normally this would be a HashMap, but SecretKey is not Hash-able + // TODO: This can be HashableBase + cache: Vec<(SecretKey, Vec)>, +} + +impl WalletCache { + pub fn new() -> Self { + Self { cache: Vec::new() } + } + + /// Must be called at the start to begin tracking received coins for this secret. + pub fn track(&mut self, secret: SecretKey) { + self.cache.push((secret, Vec::new())); + } + + /// Get all coins received by this secret key + /// track() must be called on this secret before calling this or the function will panic. + pub fn get_received(&mut self, secret: &SecretKey) -> Vec { + for (other_secret, own_coins) in self.cache.iter_mut() { + if *secret == *other_secret { + // clear own_coins vec, and return current contents + return std::mem::replace(own_coins, Vec::new()) + } + } + panic!("you forget to track() this secret!"); + } + + pub fn try_decrypt_note( + &mut self, + coin: Coin, + ciphertext: EncryptedNote2, + tree: &mut MerkleTree, + ) { + // Loop through all our secret keys... + for (secret, own_coins) in self.cache.iter_mut() { + // .. attempt to decrypt the note ... + if let Ok(note) = ciphertext.decrypt(secret) { + let leaf_position = tree.witness().expect("coin should be in tree"); + own_coins.push(OwnCoin { coin, note, leaf_position }); + } + } + } +} + +/// The state machine, held in memory. +pub struct State { + /// The entire Merkle tree state + pub tree: MerkleTree, + /// List of all previous and the current Merkle roots. + /// This is the hashed value of all the children. + pub merkle_roots: Vec, + /// Nullifiers prevent double spending + pub nullifiers: Vec, + + /// Public key of the cashier + pub cashier_signature_public: PublicKey, + + /// Public key of the faucet + pub faucet_signature_public: PublicKey, + + pub wallet_cache: WalletCache, +} + +impl State { + pub fn new( + cashier_signature_public: PublicKey, + faucet_signature_public: PublicKey, + ) -> Box { + Box::new(Self { + tree: MerkleTree::new(100), + merkle_roots: vec![], + nullifiers: vec![], + cashier_signature_public, + faucet_signature_public, + wallet_cache: WalletCache::new(), + }) + } + + pub fn is_valid_cashier_public_key(&self, public: &PublicKey) -> bool { + public == &self.cashier_signature_public + } + + pub fn is_valid_faucet_public_key(&self, public: &PublicKey) -> bool { + public == &self.faucet_signature_public + } + + pub fn is_valid_merkle(&self, merkle_root: &MerkleNode) -> bool { + self.merkle_roots.iter().any(|m| m == merkle_root) + } + + pub fn nullifier_exists(&self, nullifier: &Nullifier) -> bool { + self.nullifiers.iter().any(|n| n == nullifier) + } +} diff --git a/example/dao/contract/money_contract/transfer/mod.rs b/example/dao/src/contract/money/transfer/mod.rs similarity index 100% rename from example/dao/contract/money_contract/transfer/mod.rs rename to example/dao/src/contract/money/transfer/mod.rs diff --git a/example/dao/src/contract/money/transfer/validate.rs b/example/dao/src/contract/money/transfer/validate.rs new file mode 100644 index 000000000..7537d48c3 --- /dev/null +++ b/example/dao/src/contract/money/transfer/validate.rs @@ -0,0 +1,375 @@ +use std::any::{Any, TypeId}; + +use incrementalmerkletree::Tree; +use log::{debug, error}; +use pasta_curves::{group::Group, pallas}; + +use darkfi::{ + crypto::{ + coin::Coin, + keypair::PublicKey, + merkle_node::MerkleNode, + nullifier::Nullifier, + types::{DrkCircuitField, DrkTokenId, DrkValueBlind, DrkValueCommit}, + util::{pedersen_commitment_base, pedersen_commitment_u64}, + BurnRevealedValues, MintRevealedValues, + }, + Error as DarkFiError, +}; +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{ + dao, + money::{state::State, CONTRACT_ID}, + }, + note::EncryptedNote2, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, +}; + +const TARGET: &str = "money_contract::transfer::validate::state_transition()"; + +/// A struct representing a state update. +/// This gets applied on top of an existing state. +#[derive(Clone)] +pub struct Update { + /// All nullifiers in a transaction + pub nullifiers: Vec, + /// All coins in a transaction + pub coins: Vec, + /// All encrypted notes in a transaction + pub enc_notes: Vec, +} + +impl UpdateBase for Update { + fn apply(mut self: Box, states: &mut StateRegistry) { + let state = states.lookup_mut::(*CONTRACT_ID).unwrap(); + + // Extend our list of nullifiers with the ones from the update + state.nullifiers.append(&mut self.nullifiers); + + //// Update merkle tree and witnesses + for (coin, enc_note) in self.coins.into_iter().zip(self.enc_notes.into_iter()) { + // Add the new coins to the Merkle tree + let node = MerkleNode(coin.0); + state.tree.append(&node); + + // Keep track of all Merkle roots that have existed + state.merkle_roots.push(state.tree.root(0).unwrap()); + + state.wallet_cache.try_decrypt_note(coin, enc_note, &mut state.tree); + } + } +} + +pub fn state_transition( + states: &StateRegistry, + func_call_index: usize, + parent_tx: &Transaction, +) -> Result> { + // Check the public keys in the clear inputs to see if they're coming + // from a valid cashier or faucet. + debug!(target: TARGET, "Iterate clear_inputs"); + let func_call = &parent_tx.func_calls[func_call_index]; + let call_data = func_call.call_data.as_any(); + + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::(); + + // This will be inside wasm so unwrap is fine. + let call_data = call_data.unwrap(); + + let state = states.lookup::(*CONTRACT_ID).expect("Return type is not of type State"); + + // Code goes here + for (i, input) in call_data.clear_inputs.iter().enumerate() { + let pk = &input.signature_public; + // TODO: this depends on the token ID + if !state.is_valid_cashier_public_key(pk) && !state.is_valid_faucet_public_key(pk) { + error!(target: TARGET, "Invalid pubkey for clear input: {:?}", pk); + return Err(Error::VerifyFailed(VerifyFailed::InvalidCashierOrFaucetKey(i))) + } + } + + // Nullifiers in the transaction + let mut nullifiers = Vec::with_capacity(call_data.inputs.len()); + + debug!(target: TARGET, "Iterate inputs"); + for (i, input) in call_data.inputs.iter().enumerate() { + let merkle = &input.revealed.merkle_root; + + // The Merkle root is used to know whether this is a coin that + // existed in a previous state. + if !state.is_valid_merkle(merkle) { + error!(target: TARGET, "Invalid Merkle root (input {})", i); + debug!(target: TARGET, "root: {:?}", merkle); + return Err(Error::VerifyFailed(VerifyFailed::InvalidMerkle(i))) + } + + // Check the spend_hook is satisfied + // The spend_hook says a coin must invoke another contract function when being spent + // If the value is set, then we check the function call exists + let spend_hook = &input.revealed.spend_hook; + if spend_hook != &pallas::Base::from(0) { + // spend_hook is set so we enforce the rules + let mut is_found = false; + for (i, func_call) in parent_tx.func_calls.iter().enumerate() { + // Skip current func_call + if i == func_call_index { + continue + } + + // TODO: we need to change these to pallas::Base + // temporary workaround for now + // if func_call.func_id == spend_hook ... + if func_call.func_id == *dao::exec::FUNC_ID { + is_found = true; + break + } + } + if !is_found { + return Err(Error::VerifyFailed(VerifyFailed::SpendHookNotSatisfied)) + } + } + + // The nullifiers should not already exist. + // It is the double-spend protection. + let nullifier = &input.revealed.nullifier; + if state.nullifier_exists(nullifier) || + (1..nullifiers.len()).any(|i| nullifiers[i..].contains(&nullifiers[i - 1])) + { + error!(target: TARGET, "Duplicate nullifier found (input {})", i); + debug!(target: TARGET, "nullifier: {:?}", nullifier); + return Err(Error::VerifyFailed(VerifyFailed::NullifierExists(i))) + } + + nullifiers.push(input.revealed.nullifier); + } + + debug!(target: TARGET, "Verifying call data"); + match call_data.verify() { + Ok(()) => { + debug!(target: TARGET, "Verified successfully") + } + Err(e) => { + error!(target: TARGET, "Failed verifying zk proofs: {}", e); + return Err(Error::VerifyFailed(VerifyFailed::ProofVerifyFailed(e.to_string()))) + } + } + + // Newly created coins for this transaction + let mut coins = Vec::with_capacity(call_data.outputs.len()); + let mut enc_notes = Vec::with_capacity(call_data.outputs.len()); + + for output in &call_data.outputs { + // Gather all the coins + coins.push(output.revealed.coin); + enc_notes.push(output.enc_note.clone()); + } + + Ok(Box::new(Update { nullifiers, coins, enc_notes })) +} + +/// A DarkFi transaction +#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)] +pub struct CallData { + /// Clear inputs + pub clear_inputs: Vec, + /// Anonymous inputs + pub inputs: Vec, + /// Anonymous outputs + pub outputs: Vec, +} + +impl CallDataBase for CallData { + fn zk_public_values(&self) -> Vec<(String, Vec)> { + let mut public_values = Vec::new(); + for input in &self.inputs { + public_values.push(("money-transfer-burn".to_string(), input.revealed.make_outputs())); + } + for output in &self.outputs { + public_values.push(("money-transfer-mint".to_string(), output.revealed.make_outputs())); + } + public_values + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn signature_public_keys(&self) -> Vec { + let mut signature_public_keys = Vec::new(); + for input in self.clear_inputs.clone() { + signature_public_keys.push(input.signature_public); + } + signature_public_keys + } + + fn encode_bytes( + &self, + mut writer: &mut dyn std::io::Write, + ) -> std::result::Result { + self.encode(&mut writer) + } +} +impl CallData { + /// Verify the transaction + pub fn verify(&self) -> VerifyResult<()> { + // must have minimum 1 clear or anon input, and 1 output + if self.clear_inputs.len() + self.inputs.len() == 0 { + error!("tx::verify(): Missing inputs"); + return Err(VerifyFailed::LackingInputs) + } + if self.outputs.len() == 0 { + error!("tx::verify(): Missing outputs"); + return Err(VerifyFailed::LackingOutputs) + } + + // Accumulator for the value commitments + let mut valcom_total = DrkValueCommit::identity(); + + // Add values from the clear inputs + for input in &self.clear_inputs { + valcom_total += pedersen_commitment_u64(input.value, input.value_blind); + } + // Add values from the inputs + for input in &self.inputs { + valcom_total += &input.revealed.value_commit; + } + // Subtract values from the outputs + for output in &self.outputs { + valcom_total -= &output.revealed.value_commit; + } + + // If the accumulator is not back in its initial state, + // there's a value mismatch. + if valcom_total != DrkValueCommit::identity() { + error!("tx::verify(): Missing funds"); + return Err(VerifyFailed::MissingFunds) + } + + // Verify that the token commitments match + if !self.verify_token_commitments() { + error!("tx::verify(): Token ID mismatch"); + return Err(VerifyFailed::TokenMismatch) + } + + Ok(()) + } + + fn verify_token_commitments(&self) -> bool { + assert_ne!(self.outputs.len(), 0); + let token_commit_value = self.outputs[0].revealed.token_commit; + + let mut failed = + self.inputs.iter().any(|input| input.revealed.token_commit != token_commit_value); + + failed = failed || + self.outputs.iter().any(|output| output.revealed.token_commit != token_commit_value); + + failed = failed || + self.clear_inputs.iter().any(|input| { + pedersen_commitment_base(input.token_id, input.token_blind) != token_commit_value + }); + !failed + } +} + +/// A transaction's clear input +#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)] +pub struct ClearInput { + /// Input's value (amount) + pub value: u64, + /// Input's token ID + pub token_id: DrkTokenId, + /// Blinding factor for `value` + pub value_blind: DrkValueBlind, + /// Blinding factor for `token_id` + pub token_blind: DrkValueBlind, + /// Public key for the signature + pub signature_public: PublicKey, +} + +/// A transaction's anonymous input +#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)] +pub struct Input { + /// Public inputs for the zero-knowledge proof + pub revealed: BurnRevealedValues, +} + +/// A transaction's anonymous output +#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)] +pub struct Output { + /// Public inputs for the zero-knowledge proof + pub revealed: MintRevealedValues, + /// The encrypted note + pub enc_note: EncryptedNote2, +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error(transparent)] + VerifyFailed(#[from] VerifyFailed), + + #[error("DarkFi error: {0}")] + DarkFiError(String), +} + +/// Transaction verification errors +#[derive(Debug, Clone, thiserror::Error)] +pub enum VerifyFailed { + #[error("Transaction has no inputs")] + LackingInputs, + + #[error("Transaction has no outputs")] + LackingOutputs, + + #[error("Invalid cashier/faucet public key for clear input {0}")] + InvalidCashierOrFaucetKey(usize), + + #[error("Invalid Merkle root for input {0}")] + InvalidMerkle(usize), + + #[error("Spend hook invoking function is not attached")] + SpendHookNotSatisfied, + + #[error("Nullifier already exists for input {0}")] + NullifierExists(usize), + + #[error("Token commitments in inputs or outputs to not match")] + TokenMismatch, + + #[error("Money in does not match money out (value commitments)")] + MissingFunds, + + #[error("Failed verifying zk proofs: {0}")] + ProofVerifyFailed(String), + + #[error("Internal error: {0}")] + InternalError(String), + + #[error("DarkFi error: {0}")] + DarkFiError(String), +} + +type Result = std::result::Result; + +impl From for VerifyFailed { + fn from(err: Error) -> Self { + Self::InternalError(err.to_string()) + } +} + +impl From for VerifyFailed { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} + +impl From for Error { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} +/// Result type used in transaction verifications +pub type VerifyResult = std::result::Result; diff --git a/example/dao/src/contract/money/transfer/wallet.rs b/example/dao/src/contract/money/transfer/wallet.rs new file mode 100644 index 000000000..afbb7f1d1 --- /dev/null +++ b/example/dao/src/contract/money/transfer/wallet.rs @@ -0,0 +1,219 @@ +use pasta_curves::group::ff::Field; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + burn_proof::create_burn_proof, + keypair::{PublicKey, SecretKey}, + merkle_node::MerkleNode, + mint_proof::create_mint_proof, + types::{ + DrkCoinBlind, DrkSerial, DrkSpendHook, DrkTokenId, DrkUserData, DrkUserDataBlind, + DrkValueBlind, + }, + }, + Result, +}; +use darkfi_serial::{SerialDecodable, SerialEncodable}; + +use crate::{ + contract::money::{ + transfer::validate::{CallData, ClearInput, Input, Output}, + CONTRACT_ID, + }, + note, + util::{FuncCall, ZkContractInfo, ZkContractTable}, +}; + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Note { + pub serial: DrkSerial, + pub value: u64, + pub token_id: DrkTokenId, + pub spend_hook: DrkSpendHook, + pub user_data: DrkUserData, + pub coin_blind: DrkCoinBlind, + pub value_blind: DrkValueBlind, + pub token_blind: DrkValueBlind, +} + +pub struct Builder { + pub clear_inputs: Vec, + pub inputs: Vec, + pub outputs: Vec, +} + +pub struct BuilderClearInputInfo { + pub value: u64, + pub token_id: DrkTokenId, + pub signature_secret: SecretKey, +} + +pub struct BuilderInputInfo { + pub leaf_position: incrementalmerkletree::Position, + pub merkle_path: Vec, + pub secret: SecretKey, + pub note: Note, + pub user_data_blind: DrkUserDataBlind, + pub value_blind: DrkValueBlind, + pub signature_secret: SecretKey, +} + +pub struct BuilderOutputInfo { + pub value: u64, + pub token_id: DrkTokenId, + pub public: PublicKey, + pub serial: DrkSerial, + pub coin_blind: DrkCoinBlind, + pub spend_hook: DrkSpendHook, + pub user_data: DrkUserData, +} + +impl Builder { + fn compute_remainder_blind( + clear_inputs: &[ClearInput], + input_blinds: &[DrkValueBlind], + output_blinds: &[DrkValueBlind], + ) -> DrkValueBlind { + let mut total = DrkValueBlind::zero(); + + for input in clear_inputs { + total += input.value_blind; + } + + for input_blind in input_blinds { + total += input_blind; + } + + for output_blind in output_blinds { + total -= output_blind; + } + + total + } + + pub fn build(self, zk_bins: &ZkContractTable) -> Result { + assert!(self.clear_inputs.len() + self.inputs.len() > 0); + + let mut clear_inputs = vec![]; + let token_blind = DrkValueBlind::random(&mut OsRng); + for input in &self.clear_inputs { + let signature_public = PublicKey::from_secret(input.signature_secret); + let value_blind = DrkValueBlind::random(&mut OsRng); + + let clear_input = ClearInput { + value: input.value, + token_id: input.token_id, + value_blind, + token_blind, + signature_public, + }; + clear_inputs.push(clear_input); + } + + let mut proofs = vec![]; + let mut inputs = vec![]; + let mut input_blinds = vec![]; + + for input in self.inputs { + let value_blind = input.value_blind; + input_blinds.push(value_blind); + + let zk_info = zk_bins.lookup(&"money-transfer-burn".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Native(info) = zk_info { + info + } else { + panic!("Not native info") + }; + let burn_pk = &zk_info.proving_key; + + // Note from the previous output + let note = input.note.clone(); + + let (burn_proof, revealed) = create_burn_proof( + burn_pk, + note.value, + note.token_id, + value_blind, + token_blind, + note.serial, + note.spend_hook, + note.user_data, + input.user_data_blind, + note.coin_blind, + input.secret, + input.leaf_position, + input.merkle_path.clone(), + input.signature_secret, + )?; + proofs.push(burn_proof); + + let input = Input { revealed }; + inputs.push(input); + } + + let mut outputs = vec![]; + let mut output_blinds = vec![]; + // This value_blind calc assumes there will always be at least a single output + assert!(self.outputs.len() > 0); + + for (i, output) in self.outputs.iter().enumerate() { + let value_blind = if i == self.outputs.len() - 1 { + Self::compute_remainder_blind(&clear_inputs, &input_blinds, &output_blinds) + } else { + DrkValueBlind::random(&mut OsRng) + }; + output_blinds.push(value_blind); + + let serial = output.serial; + let coin_blind = output.coin_blind; + + let zk_info = zk_bins.lookup(&"money-transfer-mint".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Native(info) = zk_info { + info + } else { + panic!("Not native info") + }; + let mint_pk = &zk_info.proving_key; + + let (mint_proof, revealed) = create_mint_proof( + mint_pk, + output.value, + output.token_id, + value_blind, + token_blind, + serial, + output.spend_hook, + output.user_data, + coin_blind, + output.public, + )?; + proofs.push(mint_proof); + + let note = Note { + serial, + value: output.value, + token_id: output.token_id, + spend_hook: output.spend_hook, + user_data: output.user_data, + coin_blind, + value_blind, + token_blind, + }; + + let encrypted_note = note::encrypt(¬e, &output.public)?; + + let output = Output { revealed, enc_note: encrypted_note }; + outputs.push(output); + } + + let call_data = CallData { clear_inputs, inputs, outputs }; + + Ok(FuncCall { + contract_id: *CONTRACT_ID, + func_id: *super::FUNC_ID, + call_data: Box::new(call_data), + proofs, + }) + } +} diff --git a/example/dao/src/dao.rs b/example/dao/src/dao.rs new file mode 100644 index 000000000..618e88cf2 --- /dev/null +++ b/example/dao/src/dao.rs @@ -0,0 +1,1139 @@ +use async_std::sync::{Arc, Mutex}; +use std::{ + any::{Any, TypeId}, + collections::HashMap, + hash::Hasher, + io, + time::Instant, +}; + +use darkfi_serial::Encodable; +use incrementalmerkletree::Tree; +use log::{debug, info}; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{ + ff::{Field, PrimeField}, + Curve, Group, + }, + pallas, +}; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + keypair::{Keypair, PublicKey, SecretKey}, + proof::{ProvingKey, VerifyingKey}, + schnorr::{SchnorrPublic, SchnorrSecret, Signature}, + types::{DrkCircuitField, DrkSpendHook, DrkUserData, DrkValue}, + util::{pedersen_commitment_u64, poseidon_hash}, + Proof, + }, + zk::{ + circuit::{BurnContract, MintContract}, + vm::ZkCircuit, + vm_stack::empty_witnesses, + }, + zkas::decoder::ZkBinary, +}; + +mod contract; +mod error; +mod note; +mod util; + +use crate::{ + contract::{dao, example, money}, + util::{sign, StateRegistry, Transaction, ZkContractTable}, +}; + +// TODO: Anonymity leaks in this proof of concept: +// +// * Vote updates are linked to the proposal_bulla +// * Nullifier of vote will link vote with the coin when it's spent + +// TODO: strategize and cleanup Result/Error usage +// TODO: fix up code doc + +type Result = std::result::Result>; + +/////////////////////////////////////////////////// +///// Example contract +/////////////////////////////////////////////////// +pub async fn example() -> Result<()> { + debug!(target: "demo", "Stage 0. Example contract"); + // Lookup table for smart contract states + let mut states = StateRegistry::new(); + + // Initialize ZK binary table + let mut zk_bins = ZkContractTable::new(); + + let zk_example_foo_bincode = include_bytes!("../proof/foo.zk.bin"); + let zk_example_foo_bin = ZkBinary::decode(zk_example_foo_bincode)?; + zk_bins.add_contract("example-foo".to_string(), zk_example_foo_bin, 13); + + let example_state = example::state::State::new(); + states.register(*example::CONTRACT_ID, example_state); + + //// Wallet + + let foo_w = example::foo::wallet::Foo { a: 5, b: 10 }; + let signature_secret = SecretKey::random(&mut OsRng); + + let builder = example::foo::wallet::Builder { foo: foo_w, signature_secret }; + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign([signature_secret].to_vec(), &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *example::foo::FUNC_ID { + debug!("example::foo::state_transition()"); + + let update = example::foo::validate::state_transition(&states, idx, &tx) + .expect("example::foo::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + Ok(()) +} + +#[async_std::main] +async fn main() -> Result<()> { + env_logger::init(); + + // Example smart contract + //// TODO: this will be moved to a different file + example().await?; + + // Money parameters + let xdrk_supply = 1_000_000; + let xdrk_token_id = pallas::Base::random(&mut OsRng); + + // Governance token parameters + let gdrk_supply = 1_000_000; + let gdrk_token_id = pallas::Base::random(&mut OsRng); + + // DAO parameters + let dao_proposer_limit = 110; + let dao_quorum = 110; + let dao_approval_ratio_quot = 1; + let dao_approval_ratio_base = 2; + + // Lookup table for smart contract states + let mut states = StateRegistry::new(); + + // Initialize ZK binary table + let mut zk_bins = ZkContractTable::new(); + + debug!(target: "demo", "Loading dao-mint.zk"); + let zk_dao_mint_bincode = include_bytes!("../proof/dao-mint.zk.bin"); + let zk_dao_mint_bin = ZkBinary::decode(zk_dao_mint_bincode)?; + zk_bins.add_contract("dao-mint".to_string(), zk_dao_mint_bin, 13); + + debug!(target: "demo", "Loading money-transfer contracts"); + { + let start = Instant::now(); + let mint_pk = ProvingKey::build(11, &MintContract::default()); + debug!("Mint PK: [{:?}]", start.elapsed()); + let start = Instant::now(); + let burn_pk = ProvingKey::build(11, &BurnContract::default()); + debug!("Burn PK: [{:?}]", start.elapsed()); + let start = Instant::now(); + let mint_vk = VerifyingKey::build(11, &MintContract::default()); + debug!("Mint VK: [{:?}]", start.elapsed()); + let start = Instant::now(); + let burn_vk = VerifyingKey::build(11, &BurnContract::default()); + debug!("Burn VK: [{:?}]", start.elapsed()); + + zk_bins.add_native("money-transfer-mint".to_string(), mint_pk, mint_vk); + zk_bins.add_native("money-transfer-burn".to_string(), burn_pk, burn_vk); + } + debug!(target: "demo", "Loading dao-propose-main.zk"); + let zk_dao_propose_main_bincode = include_bytes!("../proof/dao-propose-main.zk.bin"); + let zk_dao_propose_main_bin = ZkBinary::decode(zk_dao_propose_main_bincode)?; + zk_bins.add_contract("dao-propose-main".to_string(), zk_dao_propose_main_bin, 13); + debug!(target: "demo", "Loading dao-propose-burn.zk"); + let zk_dao_propose_burn_bincode = include_bytes!("../proof/dao-propose-burn.zk.bin"); + let zk_dao_propose_burn_bin = ZkBinary::decode(zk_dao_propose_burn_bincode)?; + zk_bins.add_contract("dao-propose-burn".to_string(), zk_dao_propose_burn_bin, 13); + debug!(target: "demo", "Loading dao-vote-main.zk"); + let zk_dao_vote_main_bincode = include_bytes!("../proof/dao-vote-main.zk.bin"); + let zk_dao_vote_main_bin = ZkBinary::decode(zk_dao_vote_main_bincode)?; + zk_bins.add_contract("dao-vote-main".to_string(), zk_dao_vote_main_bin, 13); + debug!(target: "demo", "Loading dao-vote-burn.zk"); + let zk_dao_vote_burn_bincode = include_bytes!("../proof/dao-vote-burn.zk.bin"); + let zk_dao_vote_burn_bin = ZkBinary::decode(zk_dao_vote_burn_bincode)?; + zk_bins.add_contract("dao-vote-burn".to_string(), zk_dao_vote_burn_bin, 13); + let zk_dao_exec_bincode = include_bytes!("../proof/dao-exec.zk.bin"); + let zk_dao_exec_bin = ZkBinary::decode(zk_dao_exec_bincode)?; + zk_bins.add_contract("dao-exec".to_string(), zk_dao_exec_bin, 13); + + // State for money contracts + let cashier_signature_secret = SecretKey::random(&mut OsRng); + let cashier_signature_public = PublicKey::from_secret(cashier_signature_secret); + let faucet_signature_secret = SecretKey::random(&mut OsRng); + let faucet_signature_public = PublicKey::from_secret(faucet_signature_secret); + + /////////////////////////////////////////////////// + + let money_state = money::state::State::new(cashier_signature_public, faucet_signature_public); + states.register(*money::CONTRACT_ID, money_state); + + ///////////////////////////////////////////////////// + + let dao_state = dao::State::new(); + states.register(*dao::CONTRACT_ID, dao_state); + + ///////////////////////////////////////////////////// + ////// Create the DAO bulla + ///////////////////////////////////////////////////// + debug!(target: "demo", "Stage 1. Creating DAO bulla"); + + //// Wallet + + //// Setup the DAO + let dao_keypair = Keypair::random(&mut OsRng); + let dao_bulla_blind = pallas::Base::random(&mut OsRng); + + let signature_secret = SecretKey::random(&mut OsRng); + // Create DAO mint tx + let builder = dao::mint::wallet::Builder { + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + gov_token_id: gdrk_token_id, + dao_pubkey: dao_keypair.public, + dao_bulla_blind, + _signature_secret: signature_secret, + }; + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign([signature_secret].to_vec(), &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + // So then the verifier will lookup the corresponding state_transition and apply + // functions based off the func_id + if func_call.func_id == *dao::mint::FUNC_ID { + debug!("dao::mint::state_transition()"); + + let update = dao::mint::validate::state_transition(&states, idx, &tx) + .expect("dao::mint::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + // Wallet stuff + + // In your wallet, wait until you see the tx confirmed before doing anything below + // So for example keep track of tx hash + //assert_eq!(tx.hash(), tx_hash); + + // We need to witness() the value in our local merkle tree + // Must be called as soon as this DAO bulla is added to the state + let dao_leaf_position = { + let state = states.lookup_mut::(*dao::CONTRACT_ID).unwrap(); + state.dao_tree.witness().unwrap() + }; + + // It might just be easier to hash it ourselves from keypair and blind... + let dao_bulla = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!((*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::().unwrap(); + call_data.dao_bulla.clone() + }; + debug!(target: "demo", "Create DAO bulla: {:?}", dao_bulla.0); + + /////////////////////////////////////////////////// + //// Mint the initial supply of treasury token + //// and send it all to the DAO directly + /////////////////////////////////////////////////// + debug!(target: "demo", "Stage 2. Minting treasury token"); + + let state = states.lookup_mut::(*money::CONTRACT_ID).unwrap(); + state.wallet_cache.track(dao_keypair.secret); + + //// Wallet + + // Address of deployed contract in our example is dao::exec::FUNC_ID + // This field is public, you can see it's being sent to a DAO + // but nothing else is visible. + // + // In the python code we wrote: + // + // spend_hook = b"0xdao_ruleset" + // + let spend_hook = *dao::exec::FUNC_ID; + // The user_data 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 + let user_data = dao_bulla.0; + + let builder = money::transfer::wallet::Builder { + clear_inputs: vec![money::transfer::wallet::BuilderClearInputInfo { + value: xdrk_supply, + token_id: xdrk_token_id, + signature_secret: cashier_signature_secret, + }], + inputs: vec![], + outputs: vec![money::transfer::wallet::BuilderOutputInfo { + value: xdrk_supply, + token_id: xdrk_token_id, + public: dao_keypair.public, + serial: pallas::Base::random(&mut OsRng), + coin_blind: pallas::Base::random(&mut OsRng), + spend_hook, + user_data, + }], + }; + + let func_call = builder.build(&zk_bins)?; + let func_calls = vec![func_call]; + + let signatures = sign([cashier_signature_secret].to_vec(), &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + // So then the verifier will lookup the corresponding state_transition and apply + // functions based off the func_id + if func_call.func_id == *money::transfer::FUNC_ID { + debug!("money::transfer::state_transition()"); + + let update = money::transfer::validate::state_transition(&states, idx, &tx) + .expect("money::transfer::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + // DAO reads the money received from the encrypted note + + let state = states.lookup_mut::(*money::CONTRACT_ID).unwrap(); + let mut recv_coins = state.wallet_cache.get_received(&dao_keypair.secret); + assert_eq!(recv_coins.len(), 1); + let dao_recv_coin = recv_coins.pop().unwrap(); + let treasury_note = dao_recv_coin.note; + + // Check the actual coin received is valid before accepting it + + let coords = dao_keypair.public.0.to_affine().coordinates().unwrap(); + let coin = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + DrkValue::from(treasury_note.value), + treasury_note.token_id, + treasury_note.serial, + treasury_note.spend_hook, + treasury_note.user_data, + treasury_note.coin_blind, + ]); + assert_eq!(coin, dao_recv_coin.coin.0); + + assert_eq!(treasury_note.spend_hook, *dao::exec::FUNC_ID); + assert_eq!(treasury_note.user_data, dao_bulla.0); + + debug!("DAO received a coin worth {} xDRK", treasury_note.value); + + /////////////////////////////////////////////////// + //// Mint the governance token + //// Send it to three hodlers + /////////////////////////////////////////////////// + debug!(target: "demo", "Stage 3. Minting governance token"); + + //// Wallet + + // Hodler 1 + let gov_keypair_1 = Keypair::random(&mut OsRng); + // Hodler 2 + let gov_keypair_2 = Keypair::random(&mut OsRng); + // Hodler 3: the tiebreaker + let gov_keypair_3 = Keypair::random(&mut OsRng); + + let state = states.lookup_mut::(*money::CONTRACT_ID).unwrap(); + state.wallet_cache.track(gov_keypair_1.secret); + state.wallet_cache.track(gov_keypair_2.secret); + state.wallet_cache.track(gov_keypair_3.secret); + + let gov_keypairs = vec![gov_keypair_1, gov_keypair_2, gov_keypair_3]; + + // Spend hook and user data disabled + let spend_hook = DrkSpendHook::from(0); + let user_data = DrkUserData::from(0); + + let output1 = money::transfer::wallet::BuilderOutputInfo { + value: 400000, + token_id: gdrk_token_id, + public: gov_keypair_1.public, + serial: pallas::Base::random(&mut OsRng), + coin_blind: pallas::Base::random(&mut OsRng), + spend_hook, + user_data, + }; + + let output2 = money::transfer::wallet::BuilderOutputInfo { + value: 400000, + token_id: gdrk_token_id, + public: gov_keypair_2.public, + serial: pallas::Base::random(&mut OsRng), + coin_blind: pallas::Base::random(&mut OsRng), + spend_hook, + user_data, + }; + + let output3 = money::transfer::wallet::BuilderOutputInfo { + value: 200000, + token_id: gdrk_token_id, + public: gov_keypair_3.public, + serial: pallas::Base::random(&mut OsRng), + coin_blind: pallas::Base::random(&mut OsRng), + spend_hook, + user_data, + }; + + assert!(2 * 400000 + 200000 == gdrk_supply); + + let builder = money::transfer::wallet::Builder { + clear_inputs: vec![money::transfer::wallet::BuilderClearInputInfo { + value: gdrk_supply, + token_id: gdrk_token_id, + signature_secret: cashier_signature_secret, + }], + inputs: vec![], + outputs: vec![output1, output2, output3], + }; + + let func_call = builder.build(&zk_bins)?; + let func_calls = vec![func_call]; + + let signatures = sign([cashier_signature_secret].to_vec(), &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + // So then the verifier will lookup the corresponding state_transition and apply + // functions based off the func_id + if func_call.func_id == *money::transfer::FUNC_ID { + debug!("money::transfer::state_transition()"); + + let update = money::transfer::validate::state_transition(&states, idx, &tx) + .expect("money::transfer::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + let mut gov_recv = vec![None, None, None]; + // Check that each person received one coin + for (i, key) in gov_keypairs.iter().enumerate() { + let gov_recv_coin = { + let state = states.lookup_mut::(*money::CONTRACT_ID).unwrap(); + let mut recv_coins = state.wallet_cache.get_received(&key.secret); + assert_eq!(recv_coins.len(), 1); + let recv_coin = recv_coins.pop().unwrap(); + let note = &recv_coin.note; + + assert_eq!(note.token_id, gdrk_token_id); + // Normal payment + assert_eq!(note.spend_hook, pallas::Base::from(0)); + assert_eq!(note.user_data, pallas::Base::from(0)); + + let coords = key.public.0.to_affine().coordinates().unwrap(); + let coin = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + DrkValue::from(note.value), + note.token_id, + note.serial, + note.spend_hook, + note.user_data, + note.coin_blind, + ]); + assert_eq!(coin, recv_coin.coin.0); + + debug!("Holder{} received a coin worth {} gDRK", i, note.value); + + recv_coin + }; + gov_recv[i] = Some(gov_recv_coin); + } + // unwrap them for this demo + let gov_recv: Vec<_> = gov_recv.into_iter().map(|r| r.unwrap()).collect(); + + /////////////////////////////////////////////////// + // DAO rules: + // 1. gov token IDs must match on all inputs + // 2. proposals must be submitted by minimum amount + // 3. all votes >= quorum + // 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 + /////////////////////////////////////////////////// + debug!(target: "demo", "Stage 4. Propose the vote"); + + //// Wallet + + // TODO: look into proposal expiry once time for voting has finished + + let user_keypair = Keypair::random(&mut OsRng); + + let (money_leaf_position, money_merkle_path) = { + let state = states.lookup::(*money::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = gov_recv[0].leaf_position; + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + // TODO: is it possible for an invalid transfer() to be constructed on exec()? + // need to look into this + let signature_secret = SecretKey::random(&mut OsRng); + let input = dao::propose::wallet::BuilderInput { + secret: gov_keypair_1.secret, + note: gov_recv[0].note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret, + }; + + let (dao_merkle_path, dao_merkle_root) = { + let state = states.lookup::(*dao::CONTRACT_ID).unwrap(); + let tree = &state.dao_tree; + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(dao_leaf_position, &root).unwrap(); + (merkle_path, root) + }; + + let dao_params = dao::mint::wallet::DaoParams { + proposer_limit: dao_proposer_limit, + quorum: dao_quorum, + approval_ratio_base: dao_approval_ratio_base, + approval_ratio_quot: dao_approval_ratio_quot, + gov_token_id: gdrk_token_id, + public_key: dao_keypair.public, + bulla_blind: dao_bulla_blind, + }; + + let proposal = dao::propose::wallet::Proposal { + dest: user_keypair.public, + amount: 1000, + serial: pallas::Base::random(&mut OsRng), + token_id: xdrk_token_id, + blind: pallas::Base::random(&mut OsRng), + }; + + let builder = dao::propose::wallet::Builder { + inputs: vec![input], + proposal, + dao: dao_params.clone(), + dao_leaf_position, + dao_merkle_path, + dao_merkle_root, + }; + + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign([signature_secret].to_vec(), &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao::propose::FUNC_ID { + debug!(target: "demo", "dao::propose::state_transition()"); + + let update = dao::propose::validate::state_transition(&states, idx, &tx) + .expect("dao::propose::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + // Read received proposal + let (proposal, proposal_bulla) = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!((*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao::propose::wallet::Note = + header.enc_note.decrypt(&dao_keypair.secret).unwrap(); + + // TODO: check it belongs to DAO bulla + + // Return the proposal info + (note.proposal, call_data.header.proposal_bulla) + }; + debug!(target: "demo", "Proposal now active!"); + debug!(target: "demo", " destination: {:?}", proposal.dest); + debug!(target: "demo", " amount: {}", proposal.amount); + debug!(target: "demo", " token_id: {:?}", proposal.token_id); + debug!(target: "demo", " dao_bulla: {:?}", dao_bulla.0); + debug!(target: "demo", "Proposal bulla: {:?}", proposal_bulla); + + /////////////////////////////////////////////////// + // Proposal is accepted! + // Start the voting + /////////////////////////////////////////////////// + + // Copying these schizo comments from python code: + // 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 + + debug!(target: "demo", "Stage 5. Start voting"); + + // We were previously saving updates here for testing + // let mut updates = vec![]; + + // User 1: YES + + let (money_leaf_position, money_merkle_path) = { + let state = states.lookup::(*money::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = gov_recv[0].leaf_position; + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + let signature_secret = SecretKey::random(&mut OsRng); + let input = dao::vote::wallet::BuilderInput { + secret: gov_keypair_1.secret, + note: gov_recv[0].note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret, + }; + + let vote_option: bool = true; + // assert!(vote_option || !vote_option); // wtf + + // We create a new keypair to encrypt the vote. + // For the demo MVP, you can just use the dao_keypair secret + let vote_keypair_1 = Keypair::random(&mut OsRng); + + let builder = dao::vote::wallet::Builder { + inputs: vec![input], + vote: dao::vote::wallet::Vote { + vote_option, + vote_option_blind: pallas::Scalar::random(&mut OsRng), + }, + vote_keypair: vote_keypair_1, + proposal: proposal.clone(), + dao: dao_params.clone(), + }; + debug!(target: "demo", "build()..."); + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign([signature_secret].to_vec(), &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao::vote::FUNC_ID { + debug!(target: "demo", "dao::vote::state_transition()"); + + let update = dao::vote::validate::state_transition(&states, idx, &tx) + .expect("dao::vote::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + // Secret vote info. Needs to be revealed at some point. + // TODO: look into verifiable encryption for notes + // TODO: look into timelock puzzle as a possibility + let vote_note_1 = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!((*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao::vote::wallet::Note = + header.enc_note.decrypt(&vote_keypair_1.secret).unwrap(); + note + }; + debug!(target: "demo", "User 1 voted!"); + debug!(target: "demo", " vote_option: {}", vote_note_1.vote.vote_option); + debug!(target: "demo", " value: {}", vote_note_1.vote_value); + + // User 2: NO + + let (money_leaf_position, money_merkle_path) = { + let state = states.lookup::(*money::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = gov_recv[1].leaf_position; + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + let signature_secret = SecretKey::random(&mut OsRng); + let input = dao::vote::wallet::BuilderInput { + secret: gov_keypair_2.secret, + note: gov_recv[1].note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret, + }; + + let vote_option: bool = false; + // assert!(vote_option || !vote_option); // wtf + + // We create a new keypair to encrypt the vote. + let vote_keypair_2 = Keypair::random(&mut OsRng); + + let builder = dao::vote::wallet::Builder { + inputs: vec![input], + vote: dao::vote::wallet::Vote { + vote_option, + vote_option_blind: pallas::Scalar::random(&mut OsRng), + }, + vote_keypair: vote_keypair_2, + proposal: proposal.clone(), + dao: dao_params.clone(), + }; + debug!(target: "demo", "build()..."); + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign([signature_secret].to_vec(), &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao::vote::FUNC_ID { + debug!(target: "demo", "dao::vote::state_transition()"); + + let update = dao::vote::validate::state_transition(&states, idx, &tx) + .expect("dao::vote::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + // Secret vote info. Needs to be revealed at some point. + // TODO: look into verifiable encryption for notes + // TODO: look into timelock puzzle as a possibility + let vote_note_2 = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!((*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao::vote::wallet::Note = + header.enc_note.decrypt(&vote_keypair_2.secret).unwrap(); + note + }; + debug!(target: "demo", "User 2 voted!"); + debug!(target: "demo", " vote_option: {}", vote_note_2.vote.vote_option); + debug!(target: "demo", " value: {}", vote_note_2.vote_value); + + // User 3: YES + + let (money_leaf_position, money_merkle_path) = { + let state = states.lookup::(*money::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = gov_recv[2].leaf_position; + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + let signature_secret = SecretKey::random(&mut OsRng); + let input = dao::vote::wallet::BuilderInput { + secret: gov_keypair_3.secret, + note: gov_recv[2].note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret, + }; + + let vote_option: bool = true; + // assert!(vote_option || !vote_option); // wtf + + // We create a new keypair to encrypt the vote. + let vote_keypair_3 = Keypair::random(&mut OsRng); + + let builder = dao::vote::wallet::Builder { + inputs: vec![input], + vote: dao::vote::wallet::Vote { + vote_option, + vote_option_blind: pallas::Scalar::random(&mut OsRng), + }, + vote_keypair: vote_keypair_3, + proposal: proposal.clone(), + dao: dao_params.clone(), + }; + debug!(target: "demo", "build()..."); + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign([signature_secret].to_vec(), &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao::vote::FUNC_ID { + debug!(target: "demo", "dao::vote::state_transition()"); + + let update = dao::vote::validate::state_transition(&states, idx, &tx) + .expect("dao::vote::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + // Secret vote info. Needs to be revealed at some point. + // TODO: look into verifiable encryption for notes + // TODO: look into timelock puzzle as a possibility + let vote_note_3 = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!((*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao::vote::wallet::Note = + header.enc_note.decrypt(&vote_keypair_3.secret).unwrap(); + note + }; + debug!(target: "demo", "User 3 voted!"); + debug!(target: "demo", " vote_option: {}", vote_note_3.vote.vote_option); + debug!(target: "demo", " value: {}", vote_note_3.vote_value); + + // 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) + + let mut yes_votes_value = 0; + let mut yes_votes_blind = pallas::Scalar::from(0); + let mut yes_votes_commit = pallas::Point::identity(); + + let mut all_votes_value = 0; + let mut all_votes_blind = pallas::Scalar::from(0); + let mut all_votes_commit = pallas::Point::identity(); + + // We were previously saving votes to a Vec for testing. + // However since Update is now UpdateBase it gets moved into update.apply(). + // So we need to think of another way to run these tests. + //assert!(updates.len() == 3); + + for (i, note /* update*/) in [vote_note_1, vote_note_2, vote_note_3] + .iter() /*.zip(updates)*/ + .enumerate() + { + let vote_commit = pedersen_commitment_u64(note.vote_value, note.vote_value_blind); + //assert!(update.value_commit == all_vote_value_commit); + all_votes_commit += vote_commit; + all_votes_blind += note.vote_value_blind; + + let yes_vote_commit = pedersen_commitment_u64( + note.vote.vote_option as u64 * note.vote_value, + note.vote.vote_option_blind, + ); + //assert!(update.yes_vote_commit == yes_vote_commit); + + yes_votes_commit += yes_vote_commit; + yes_votes_blind += note.vote.vote_option_blind; + + let vote_option = note.vote.vote_option; + + if vote_option { + yes_votes_value += note.vote_value; + } + all_votes_value += note.vote_value; + let vote_result: String = if vote_option { "yes".to_string() } else { "no".to_string() }; + + debug!("Voter {} voted {}", i, vote_result); + } + + debug!("Outcome = {} / {}", yes_votes_value, all_votes_value); + + assert!(all_votes_commit == pedersen_commitment_u64(all_votes_value, all_votes_blind)); + assert!(yes_votes_commit == pedersen_commitment_u64(yes_votes_value, yes_votes_blind)); + + /////////////////////////////////////////////////// + // Execute the vote + /////////////////////////////////////////////////// + + //// Wallet + + // Used to export user_data from this coin so it can be accessed by DAO::exec() + let user_data_blind = pallas::Base::random(&mut OsRng); + + let user_serial = pallas::Base::random(&mut OsRng); + let user_coin_blind = pallas::Base::random(&mut OsRng); + let dao_serial = pallas::Base::random(&mut OsRng); + let dao_coin_blind = pallas::Base::random(&mut OsRng); + let input_value = treasury_note.value; + let input_value_blind = pallas::Scalar::random(&mut OsRng); + let tx_signature_secret = SecretKey::random(&mut OsRng); + let exec_signature_secret = SecretKey::random(&mut OsRng); + + let (treasury_leaf_position, treasury_merkle_path) = { + let state = states.lookup::(*money::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = dao_recv_coin.leaf_position; + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + let input = money::transfer::wallet::BuilderInputInfo { + leaf_position: treasury_leaf_position, + merkle_path: treasury_merkle_path, + secret: dao_keypair.secret, + note: treasury_note, + user_data_blind, + value_blind: input_value_blind, + signature_secret: tx_signature_secret, + }; + + let builder = money::transfer::wallet::Builder { + clear_inputs: vec![], + inputs: vec![input], + outputs: vec![ + // Sending money + money::transfer::wallet::BuilderOutputInfo { + value: 1000, + token_id: xdrk_token_id, + public: user_keypair.public, + serial: proposal.serial, + coin_blind: proposal.blind, + spend_hook: pallas::Base::from(0), + user_data: pallas::Base::from(0), + }, + // Change back to DAO + money::transfer::wallet::BuilderOutputInfo { + value: xdrk_supply - 1000, + token_id: xdrk_token_id, + public: dao_keypair.public, + serial: dao_serial, + coin_blind: dao_coin_blind, + spend_hook: *dao::exec::FUNC_ID, + // TODO: should be DAO bulla + user_data: proposal_bulla, + }, + ], + }; + + let transfer_func_call = builder.build(&zk_bins)?; + + let builder = dao::exec::wallet::Builder { + proposal, + dao: dao_params, + yes_votes_value, + all_votes_value, + yes_votes_blind, + all_votes_blind, + user_serial, + user_coin_blind, + dao_serial, + dao_coin_blind, + input_value, + input_value_blind, + hook_dao_exec: *dao::exec::FUNC_ID, + signature_secret: exec_signature_secret, + }; + let exec_func_call = builder.build(&zk_bins); + let func_calls = vec![transfer_func_call, exec_func_call]; + + let signatures = sign([tx_signature_secret, exec_signature_secret].to_vec(), &func_calls); + let tx = Transaction { func_calls, signatures }; + + { + // Now the spend_hook field specifies the function DAO::exec() + // so Money::transfer() must also be combined with DAO::exec() + + assert_eq!(tx.func_calls.len(), 2); + let transfer_func_call = &tx.func_calls[0]; + let transfer_call_data = transfer_func_call.call_data.as_any(); + + assert_eq!( + (*transfer_call_data).type_id(), + TypeId::of::() + ); + let transfer_call_data = + transfer_call_data.downcast_ref::(); + let transfer_call_data = transfer_call_data.unwrap(); + // At least one input has this field value which means DAO::exec() is invoked. + assert_eq!(transfer_call_data.inputs.len(), 1); + let input = &transfer_call_data.inputs[0]; + assert_eq!(input.revealed.spend_hook, *dao::exec::FUNC_ID); + let user_data_enc = poseidon_hash::<2>([dao_bulla.0, user_data_blind]); + assert_eq!(input.revealed.user_data_enc, user_data_enc); + } + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao::exec::FUNC_ID { + debug!("dao::exec::state_transition()"); + + let update = dao::exec::validate::state_transition(&states, idx, &tx) + .expect("dao::exec::validate::state_transition() failed!"); + updates.push(update); + } else if func_call.func_id == *money::transfer::FUNC_ID { + debug!("money::transfer::state_transition()"); + + let update = money::transfer::validate::state_transition(&states, idx, &tx) + .expect("money::transfer::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + // Other stuff + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + Ok(()) +} diff --git a/example/dao/src/error.rs b/example/dao/src/error.rs new file mode 100644 index 000000000..d33356221 --- /dev/null +++ b/example/dao/src/error.rs @@ -0,0 +1,56 @@ +use serde_json::Value; + +use darkfi::rpc::jsonrpc::{ErrorCode::ServerError, JsonError, JsonResult}; + +#[derive(Debug, thiserror::Error)] +pub enum DaoError { + #[error("No Proposals found")] + NoProposals, + #[error("No DAO params found")] + DaoNotConfigured, + #[error("State transition failed: '{0}'")] + StateTransitionFailed(String), + #[error("Wallet does not exist")] + NoWalletFound, + #[error("State not found")] + StateNotFound, + #[error("InternalError")] + Darkfi(#[from] darkfi::error::Error), + #[error("Verify proof failed: '{0}', '{0}'")] + VerifyProofFailed(usize, String), +} + +pub type DaoResult = std::result::Result; + +pub enum RpcError { + Vote = -32101, + Propose = -32102, + Exec = -32103, + Airdrop = -32104, + Mint = -32105, + Keygen = -32106, + Create = -32107, + Parse = -32108, + Balance = -32109, +} + +fn to_tuple(e: RpcError) -> (i64, String) { + let msg = match e { + RpcError::Vote => "Failed to cast a Vote", + RpcError::Propose => "Failed to generate a Proposal", + RpcError::Airdrop => "Failed to transfer an airdrop", + RpcError::Keygen => "Failed to generate keypair", + RpcError::Create => "Failed to create DAO", + RpcError::Exec => "Failed to execute Proposal", + RpcError::Mint => "Failed to mint DAO treasury", + RpcError::Parse => "Generic parsing error", + RpcError::Balance => "Failed to get balance", + }; + + (e as i64, msg.to_string()) +} + +pub fn server_error(e: RpcError, id: Value) -> JsonResult { + let (code, msg) = to_tuple(e); + JsonError::new(ServerError(code), Some(msg), id).into() +} diff --git a/example/dao/note.rs b/example/dao/src/note.rs similarity index 98% rename from example/dao/note.rs rename to example/dao/src/note.rs index 46ba2732c..b5f72e68f 100644 --- a/example/dao/note.rs +++ b/example/dao/src/note.rs @@ -1,5 +1,4 @@ use crypto_api_chachapoly::ChachaPolyIetf; -use darkfi_serial::{Decodable, Encodable, SerialDecodable, SerialEncodable}; use rand::rngs::OsRng; use darkfi::{ @@ -9,6 +8,7 @@ use darkfi::{ }, Error, Result, }; +use darkfi_serial::{Decodable, Encodable, SerialDecodable, SerialEncodable}; pub const AEAD_TAG_SIZE: usize = 16; @@ -51,8 +51,7 @@ impl EncryptedNote2 { self.ciphertext.len() - AEAD_TAG_SIZE ); - let t = T::decode(&plaintext[..])?; - Ok(t) + T::decode(&plaintext[..]).map_err(Error::from) } } diff --git a/example/dao/src/util.rs b/example/dao/src/util.rs new file mode 100644 index 000000000..30780e597 --- /dev/null +++ b/example/dao/src/util.rs @@ -0,0 +1,265 @@ +use std::{any::Any, collections::HashMap, hash::Hasher}; + +use lazy_static::lazy_static; +use log::debug; +use pasta_curves::{ + group::ff::{Field, PrimeField}, + pallas, +}; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + keypair::{PublicKey, SecretKey}, + proof::{ProvingKey, VerifyingKey}, + schnorr::{SchnorrPublic, SchnorrSecret, Signature}, + types::DrkCircuitField, + Proof, + }, + zk::{vm::ZkCircuit, vm_stack::empty_witnesses}, + zkas::decoder::ZkBinary, + Error, +}; +use darkfi_serial::Encodable; + +use crate::error::{DaoError, DaoResult}; + +/// Parse pallas::Base from a base58-encoded string +pub fn parse_b58(s: &str) -> std::result::Result { + let bytes = bs58::decode(s).into_vec()?; + if bytes.len() != 32 { + return Err(Error::ParseFailed("Failed parsing DrkTokenId from base58 string")) + } + + let ret = pallas::Base::from_repr(bytes.try_into().unwrap()); + if ret.is_some().unwrap_u8() == 1 { + return Ok(ret.unwrap()) + } + + Err(Error::ParseFailed("Failed parsing DrkTokenId from base58 string")) +} + +// The token of the DAO treasury. +lazy_static! { + pub static ref DRK_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} + +// Governance tokens that are airdropped to users to operate the DAO. +lazy_static! { + pub static ref GOV_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} + +#[derive(Eq, PartialEq, Debug)] +pub struct HashableBase(pub pallas::Base); + +impl std::hash::Hash for HashableBase { + fn hash(&self, state: &mut H) { + let bytes = self.0.to_repr(); + bytes.hash(state); + } +} + +#[derive(Clone)] +pub struct ZkBinaryContractInfo { + pub k_param: u32, + pub bincode: ZkBinary, + pub proving_key: ProvingKey, + pub verifying_key: VerifyingKey, +} + +#[derive(Clone)] +pub struct ZkNativeContractInfo { + pub proving_key: ProvingKey, + pub verifying_key: VerifyingKey, +} + +#[derive(Clone)] +pub enum ZkContractInfo { + Binary(ZkBinaryContractInfo), + Native(ZkNativeContractInfo), +} + +#[derive(Clone)] +pub struct ZkContractTable { + // Key will be a hash of zk binary contract on chain + table: HashMap, +} + +impl ZkContractTable { + pub fn new() -> Self { + Self { table: HashMap::new() } + } + + pub fn add_contract(&mut self, key: String, bincode: ZkBinary, k_param: u32) { + let witnesses = empty_witnesses(&bincode); + let circuit = ZkCircuit::new(witnesses, bincode.clone()); + let proving_key = ProvingKey::build(k_param, &circuit); + let verifying_key = VerifyingKey::build(k_param, &circuit); + let info = ZkContractInfo::Binary(ZkBinaryContractInfo { + k_param, + bincode, + proving_key, + verifying_key, + }); + self.table.insert(key, info); + } + + pub fn add_native( + &mut self, + key: String, + proving_key: ProvingKey, + verifying_key: VerifyingKey, + ) { + self.table.insert( + key, + ZkContractInfo::Native(ZkNativeContractInfo { proving_key, verifying_key }), + ); + } + + pub fn lookup(&self, key: &String) -> Option<&ZkContractInfo> { + self.table.get(key) + } +} + +pub struct Transaction { + pub func_calls: Vec, + pub signatures: Vec, +} + +impl Transaction { + /// Verify ZK contracts for the entire tx + /// In real code, we could parallelize this for loop + /// TODO: fix use of unwrap with Result type stuff + pub fn zk_verify(&self, zk_bins: &ZkContractTable) -> DaoResult<()> { + for func_call in &self.func_calls { + let proofs_public_vals = &func_call.call_data.zk_public_values(); + + assert_eq!( + proofs_public_vals.len(), + func_call.proofs.len(), + "proof_public_vals.len()={} and func_call.proofs.len()={} do not match", + proofs_public_vals.len(), + func_call.proofs.len() + ); + for (i, (proof, (key, public_vals))) in + func_call.proofs.iter().zip(proofs_public_vals.iter()).enumerate() + { + match zk_bins.lookup(key).unwrap() { + ZkContractInfo::Binary(info) => { + let verifying_key = &info.verifying_key; + let verify_result = proof.verify(&verifying_key, public_vals); + if verify_result.is_err() { + return Err(DaoError::VerifyProofFailed(i, key.to_string())) + } + //assert!(verify_result.is_ok(), "verify proof[{}]='{}' failed", i, key); + } + ZkContractInfo::Native(info) => { + let verifying_key = &info.verifying_key; + let verify_result = proof.verify(&verifying_key, public_vals); + if verify_result.is_err() { + return Err(DaoError::VerifyProofFailed(i, key.to_string())) + } + //assert!(verify_result.is_ok(), "verify proof[{}]='{}' failed", i, key); + } + }; + debug!(target: "demo", "zk_verify({}) passed [i={}]", key, i); + } + } + Ok(()) + } + + pub fn verify_sigs(&self) { + let mut unsigned_tx_data = vec![]; + for (i, (func_call, signature)) in + self.func_calls.iter().zip(self.signatures.clone()).enumerate() + { + func_call.encode(&mut unsigned_tx_data).expect("failed to encode data"); + let signature_pub_keys = func_call.call_data.signature_public_keys(); + for signature_pub_key in signature_pub_keys { + let verify_result = signature_pub_key.verify(&unsigned_tx_data[..], &signature); + assert!(verify_result, "verify sigs[{}] failed", i); + } + debug!(target: "demo", "verify_sigs({}) passed", i); + } + } +} + +pub fn sign(signature_secrets: Vec, func_calls: &Vec) -> Vec { + let mut signatures = vec![]; + let mut unsigned_tx_data = vec![]; + for (_i, (signature_secret, func_call)) in + signature_secrets.iter().zip(func_calls.iter()).enumerate() + { + func_call.encode(&mut unsigned_tx_data).expect("failed to encode data"); + let signature = signature_secret.sign(&unsigned_tx_data[..]); + signatures.push(signature); + } + signatures +} + +type ContractId = pallas::Base; +type FuncId = pallas::Base; + +pub struct FuncCall { + pub contract_id: ContractId, + pub func_id: FuncId, + pub call_data: Box, + pub proofs: Vec, +} + +impl Encodable for FuncCall { + fn encode(&self, mut w: W) -> std::result::Result { + let mut len = 0; + len += self.contract_id.encode(&mut w)?; + len += self.func_id.encode(&mut w)?; + len += self.proofs.encode(&mut w)?; + len += self.call_data.encode_bytes(&mut w)?; + Ok(len) + } +} + +pub trait CallDataBase { + // Public values for verifying the proofs + // Needed so we can convert internal types so they can be used in Proof::verify() + fn zk_public_values(&self) -> Vec<(String, Vec)>; + + // For upcasting to CallData itself so it can be read in state_transition() + fn as_any(&self) -> &dyn Any; + + // Public keys we will use to verify transaction signatures. + fn signature_public_keys(&self) -> Vec; + + fn encode_bytes( + &self, + writer: &mut dyn std::io::Write, + ) -> std::result::Result; +} + +type GenericContractState = Box; + +pub struct StateRegistry { + pub states: HashMap, +} + +impl StateRegistry { + pub fn new() -> Self { + Self { states: HashMap::new() } + } + + pub fn register(&mut self, contract_id: ContractId, state: GenericContractState) { + debug!(target: "StateRegistry::register()", "contract_id: {:?}", contract_id); + self.states.insert(HashableBase(contract_id), state); + } + + pub fn lookup_mut<'a, S: 'static>(&'a mut self, contract_id: ContractId) -> Option<&'a mut S> { + self.states.get_mut(&HashableBase(contract_id)).and_then(|state| state.downcast_mut()) + } + + pub fn lookup<'a, S: 'static>(&'a self, contract_id: ContractId) -> Option<&'a S> { + self.states.get(&HashableBase(contract_id)).and_then(|state| state.downcast_ref()) + } +} + +pub trait UpdateBase { + fn apply(self: Box, states: &mut StateRegistry); +}