main.py 12 KB

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