main.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import sys
  2. from classnamespace import ClassNamespace
  3. import crypto, money
  4. class MoneyState:
  5. def __init__(self):
  6. self.all_coins = set()
  7. self.nullifiers = set()
  8. def is_valid_merkle(self, all_coins):
  9. return all_coins.issubset(self.all_coins)
  10. def nullifier_exists(self, nullifier):
  11. return nullifier in self.nullifiers
  12. def apply(self, update):
  13. self.nullifiers = self.nullifiers.union(update.nullifiers)
  14. for coin, enc_note in zip(update.coins, update.enc_notes):
  15. self.all_coins.add(coin)
  16. def money_state_transition(state, tx):
  17. for input in tx.clear_inputs:
  18. pk = input.signature_public
  19. # Check pk is correct
  20. for input in tx.inputs:
  21. if not state.is_valid_merkle(input.revealed.all_coins):
  22. print(f"invalid merkle root", file=sys.stderr)
  23. return None
  24. nullifier = input.revealed.nullifier
  25. if state.nullifier_exists(nullifier):
  26. print(f"duplicate nullifier found", file=sys.stderr)
  27. return None
  28. is_verify, reason = tx.verify()
  29. if not is_verify:
  30. print(f"tx verify failed: {reason}", file=sys.stderr)
  31. return None
  32. update = ClassNamespace()
  33. update.nullifiers = [input.revealed.nullifier for input in tx.inputs]
  34. update.coins = [output.revealed.coin for output in tx.outputs]
  35. update.enc_notes = [output.enc_note for output in tx.outputs]
  36. return update
  37. class DaoBuilder:
  38. def __init__(self, proposal_auth_public_key, threshold, quorum, ec):
  39. self.proposal_auth_public_key = proposal_auth_public_key
  40. self.threshold = threshold
  41. self.quorum = quorum
  42. self.ec = ec
  43. def build(self):
  44. mint_proof = DaoMintProof(
  45. self.proposal_auth_public_key,
  46. self.threshold,
  47. self.quorum,
  48. self.ec
  49. )
  50. revealed = mint_proof.get_revealed()
  51. dao = Dao(revealed, mint_proof, self.ec)
  52. return dao
  53. class Dao:
  54. def __init__(self, revealed, mint_proof, ec):
  55. self.revealed = revealed
  56. self.mint_proof = mint_proof
  57. self.ec = ec
  58. def verify(self):
  59. if not self.mint_proof.verify(self.revealed):
  60. return False, "mint proof failed to verify"
  61. return True, None
  62. # class DaoExec .etc
  63. class DaoMintProof:
  64. def __init__(self, proposal_auth_public_key, threshold, quorum, ec):
  65. self.proposal_auth_public_key = proposal_auth_public_key
  66. self.threshold = threshold
  67. self.quorum = quorum
  68. self.ec = ec
  69. def get_revealed(self):
  70. revealed = ClassNamespace()
  71. revealed.bulla = crypto.ff_hash(
  72. self.ec.p,
  73. self.proposal_auth_public_key[0],
  74. self.proposal_auth_public_key[1],
  75. self.threshold,
  76. self.quorum
  77. )
  78. return revealed
  79. def verify(self, public):
  80. revealed = self.get_revealed()
  81. return True
  82. # Shared between DaoMint and DaoExec
  83. class DaoState:
  84. def __init__(self):
  85. self.bullas = set()
  86. def apply(self, update):
  87. self.bullas.add(update.bulla)
  88. def apply_exec(self, update):
  89. pass
  90. # contract interface functions
  91. def dao_state_transition(state, tx):
  92. is_verify, reason = tx.verify()
  93. if not is_verify:
  94. print(f"dao tx verify failed: {reason}", file=sys.stderr)
  95. return None
  96. update = ClassNamespace()
  97. update.bulla = tx.revealed.bulla
  98. return update
  99. ###### DAO EXEC
  100. class DaoExecBuilder:
  101. def __init__(self):
  102. pass
  103. def build(self):
  104. tx = DaoExec()
  105. return tx
  106. class DaoExec:
  107. def __init__(self):
  108. pass
  109. class DaoExecProof:
  110. def __init__(self):
  111. pass
  112. def dao_exec_state_transition(state, tx):
  113. update = ClassNamespace()
  114. return update
  115. def main(argv):
  116. ec = crypto.pallas_curve()
  117. money_state = MoneyState()
  118. gov_state = MoneyState()
  119. dao_state = DaoState()
  120. # Money parameters
  121. money_initial_supply = 21000
  122. money_token_id = 110
  123. # Governance token parameters
  124. gov_initial_supply = 10000
  125. gov_token_id = 4
  126. # DAO parameters
  127. proposal_auth_secret = ec.random_scalar()
  128. proposal_auth_public = ec.multiply(proposal_auth_secret, ec.G)
  129. threshold = 110
  130. quorum = 110
  131. ################################################
  132. # Create the DAO bulla
  133. ################################################
  134. # Setup the DAO
  135. dao_shared_secret = ec.random_scalar()
  136. dao_public_key = ec.multiply(dao_shared_secret, ec.G)
  137. builder = DaoBuilder(proposal_auth_public, threshold, quorum, ec)
  138. tx = builder.build()
  139. # Each deployment of a contract has a unique state
  140. # associated with it.
  141. if (update := dao_state_transition(dao_state, tx)) is None:
  142. return -1
  143. dao_state.apply(update)
  144. dao_bulla = tx.revealed.bulla
  145. ################################################
  146. # Mint the initial supply of treasury token
  147. # and send it all to the DAO directly
  148. ################################################
  149. # Only used for this tx. Discarded after
  150. signature_secret = ec.random_scalar()
  151. builder = money.SendPaymentTxBuilder(ec)
  152. builder.add_clear_input(money_initial_supply, money_token_id,
  153. signature_secret)
  154. # Address of deployed contract in our example is 0xdao_ruleset
  155. spend_hook = b"0xdao_ruleset"
  156. # This can be a simple hash of the items passed into the ZK proof
  157. # up to corresponding linked ZK proof to interpret however they need.
  158. # In out case, it's the bulla for the DAO
  159. user_data = dao_bulla
  160. builder.add_output(money_initial_supply, money_token_id, dao_public_key,
  161. spend_hook, user_data)
  162. tx = builder.build()
  163. # This state_transition function is the ruleset for anon payments
  164. if (update := money_state_transition(money_state, tx)) is None:
  165. return -1
  166. money_state.apply(update)
  167. # payment state transition in coin specifies dependency
  168. # the tx exists and ruleset is applied
  169. assert len(tx.outputs) > 0
  170. note = tx.outputs[0].enc_note
  171. coin = crypto.ff_hash(
  172. ec.p,
  173. dao_public_key[0],
  174. dao_public_key[1],
  175. note.value,
  176. note.token_id,
  177. note.serial,
  178. note.coin_blind,
  179. spend_hook,
  180. user_data
  181. )
  182. assert coin == tx.outputs[0].mint_proof.get_revealed().coin
  183. for coin, enc_note in zip(update.coins, update.enc_notes):
  184. # Try decrypt note here
  185. print(f"Received {enc_note.value} DRK")
  186. ################################################
  187. # Mint the governance token
  188. # Send it to two hodlers
  189. ################################################
  190. # Hodler 1
  191. gov_secret_1 = ec.random_scalar()
  192. gov_public_1 = ec.multiply(gov_secret_1, ec.G)
  193. # Hodler 2
  194. gov_secret_2 = ec.random_scalar()
  195. gov_public_2 = ec.multiply(gov_secret_2, ec.G)
  196. # Only used for this tx. Discarded after
  197. signature_secret = ec.random_scalar()
  198. builder = money.SendPaymentTxBuilder(ec)
  199. builder.add_clear_input(gov_initial_supply, gov_token_id,
  200. signature_secret)
  201. assert 2 * 5000 == gov_initial_supply
  202. builder.add_output(5000, gov_token_id, gov_public_1,
  203. b"0x0000", b"0x0000")
  204. builder.add_output(5000, gov_token_id, gov_public_1,
  205. b"0x0000", b"0x0000")
  206. tx = builder.build()
  207. # This state_transition function is the ruleset for anon payments
  208. if (update := money_state_transition(gov_state, tx)) is None:
  209. return -1
  210. gov_state.apply(update)
  211. # Decrypt output notes
  212. assert len(tx.outputs) == 2
  213. gov_user_1_note = tx.outputs[0].enc_note
  214. gov_user_2_note = tx.outputs[1].enc_note
  215. for coin, enc_note in zip(update.coins, update.enc_notes):
  216. # Try decrypt note here
  217. print(f"Received {enc_note.value} GOV")
  218. ################################################
  219. # Propose the vote
  220. # In order to make a valid vote, first the proposer must
  221. # meet a criteria for a minimum number of gov tokens
  222. ################################################
  223. # State
  224. # functions that can be called on state with params
  225. # functions return an update
  226. # optional encrypted values that can be read by wallets
  227. # --> (do this outside??)
  228. # --> penalized if fail
  229. # apply update to state
  230. # Every votes produces a semi-homomorphic encryption of their vote.
  231. # Which is either yes or no
  232. # We copy the state tree for the governance token so coins can be used
  233. # to vote on other proposals at the same time.
  234. # With their vote, they produce a ZK proof + nullifier
  235. # The votes are unblinded by MPC to a selected party at the end of the
  236. # voting period.
  237. # (that's if we want votes to be hidden during voting)
  238. votes_yes = 10
  239. votes_no = 5
  240. ################################################
  241. # Execute the vote
  242. ################################################
  243. # Used to export user_data from this coin so it can be accessed
  244. # by 0xdao_ruleset
  245. user_data_blind = ec.random_base()
  246. builder = money.SendPaymentTxBuilder(ec)
  247. witness = money_state.all_coins
  248. builder.add_input(witness, dao_shared_secret, note, user_data_blind)
  249. user_secret = ec.random_scalar()
  250. user_public = ec.multiply(user_secret, ec.G)
  251. builder.add_output(1000, money_token_id, user_public,
  252. spend_hook=b"0x0000", user_data=b"0x0000")
  253. # Change
  254. builder.add_output(note.value - 1000, money_token_id, dao_public_key,
  255. spend_hook, user_data)
  256. tx = builder.build()
  257. if (update := money_state_transition(money_state, tx)) is None:
  258. return -1
  259. money_state.apply(update)
  260. # Now the spend_hook field specifies the function DaoExec
  261. # so the tx above must also be combined with a DaoExec tx
  262. assert len(tx.inputs) == 1
  263. # At least one input has this field value which means the 0xdao_ruleset
  264. # is invoked.
  265. input = tx.inputs[0]
  266. assert input.revealed.spend_hook == b"0xdao_ruleset"
  267. assert (input.revealed.enc_user_data ==
  268. crypto.ff_hash(
  269. ec.p,
  270. user_data,
  271. user_data_blind
  272. ))
  273. # Verifier cannot see DAO bulla
  274. # They see the enc_user_data which is also in the DAO exec contract
  275. assert user_data == crypto.ff_hash(
  276. ec.p,
  277. proposal_auth_public[0],
  278. proposal_auth_public[1],
  279. threshold,
  280. quorum
  281. ) # DAO bulla
  282. # proposer proof
  283. # Now enforce DAO rules:
  284. # 1. proposals must be submitted by minimum amount
  285. # - need protection so can't collude? must be a single signer??
  286. # - stellar: doesn't have to be robust for this MVP
  287. # 2. number of votes >= quorum
  288. # - just positive votes or all votes?
  289. # - stellar: no that's all votes
  290. # 3. outcome > approval_ratio
  291. # 3. structure of outputs
  292. # output 0: value and address
  293. # output 1: change address
  294. builder = DaoExecBuilder()
  295. tx = builder.build()
  296. if (update := dao_exec_state_transition(dao_state, tx)) is None:
  297. return -1
  298. dao_state.apply_exec(update)
  299. return 0
  300. if __name__ == "__main__":
  301. sys.exit(main(sys.argv))