cli.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. #!/usr/bin/env node
  2. // Temporary demo client
  3. // Works both in browser and node.js
  4. const fs = require('fs')
  5. const assert = require('assert')
  6. const snarkjs = require('snarkjs')
  7. const crypto = require('crypto')
  8. const circomlib = require('circomlib')
  9. const bigInt = snarkjs.bigInt
  10. const merkleTree = require('./lib/MerkleTree')
  11. const Web3 = require('web3')
  12. const buildGroth16 = require('websnark/src/groth16')
  13. const websnarkUtils = require('websnark/src/utils')
  14. let web3, mixer, erc20mixer, circuit, proving_key, groth16, erc20
  15. let MERKLE_TREE_HEIGHT, ETH_AMOUNT, EMPTY_ELEMENT, ERC20_TOKEN
  16. const inBrowser = (typeof window !== 'undefined')
  17. /** Generate random number of specified byte length */
  18. const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
  19. /** Compute pedersen hash */
  20. const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
  21. /**
  22. * Create deposit object from secret and nullifier
  23. */
  24. function createDeposit(nullifier, secret) {
  25. let deposit = { nullifier, secret }
  26. deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
  27. deposit.commitment = pedersenHash(deposit.preimage)
  28. return deposit
  29. }
  30. /**
  31. * Make a deposit
  32. * @returns {Promise<string>}
  33. */
  34. async function deposit() {
  35. const deposit = createDeposit(rbigint(31), rbigint(31))
  36. console.log('Submitting deposit transaction')
  37. await mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: ETH_AMOUNT, from: (await web3.eth.getAccounts())[0], gas:4e6 })
  38. const note = '0x' + deposit.preimage.toString('hex')
  39. console.log('Your note:', note)
  40. return note
  41. }
  42. async function depositErc20() {
  43. const account = (await web3.eth.getAccounts())[0]
  44. const tokenAmount = process.env.TOKEN_AMOUNT
  45. await erc20.methods.mint(account, tokenAmount).send({ from: account, gas:1e6 })
  46. await erc20.methods.approve(erc20mixer.address, tokenAmount).send({ from: account, gas:1e6 })
  47. const allowance = await erc20.methods.allowance(account, erc20mixer.address).call()
  48. console.log('erc20mixer allowance', allowance.toString(10))
  49. const deposit = createDeposit(rbigint(31), rbigint(31))
  50. await erc20mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: ETH_AMOUNT, from: account, gas:4e6 })
  51. const balance = await erc20.methods.balanceOf(erc20mixer.address).call()
  52. console.log('erc20mixer balance', balance.toString(10))
  53. const note = '0x' + deposit.preimage.toString('hex')
  54. console.log('Your note:', note)
  55. return note
  56. }
  57. async function withdrawErc20(note, receiver, relayer) {
  58. let buf = Buffer.from(note.slice(2), 'hex')
  59. let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
  60. console.log('Getting current state from mixer contract')
  61. const events = await erc20mixer.getPastEvents('Deposit', { fromBlock: erc20mixer.deployedBlock, toBlock: 'latest' })
  62. let leafIndex
  63. const commitment = deposit.commitment.toString(16).padStart('66', '0x000000')
  64. const leaves = events
  65. .sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex))
  66. .map(e => {
  67. if (e.returnValues.commitment.eq(commitment)) {
  68. leafIndex = e.returnValues.leafIndex.toNumber()
  69. }
  70. return e.returnValues.commitment
  71. })
  72. const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
  73. const validRoot = await erc20mixer.methods.isKnownRoot(await tree.root()).call()
  74. const nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
  75. const nullifierHashToCheck = nullifierHash.toString(16).padStart('66', '0x000000')
  76. const isSpent = await erc20mixer.methods.isSpent(nullifierHashToCheck).call()
  77. assert(validRoot === true)
  78. assert(isSpent === false)
  79. assert(leafIndex >= 0)
  80. const { root, path_elements, path_index } = await tree.path(leafIndex)
  81. // Circuit input
  82. const input = {
  83. // public
  84. root: root,
  85. nullifierHash,
  86. receiver: bigInt(receiver),
  87. relayer: bigInt(relayer),
  88. fee: bigInt(web3.utils.toWei('0.01')),
  89. // private
  90. nullifier: deposit.nullifier,
  91. secret: deposit.secret,
  92. pathElements: path_elements,
  93. pathIndex: path_index,
  94. }
  95. console.log('Generating SNARK proof')
  96. console.time('Proof time')
  97. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  98. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  99. console.timeEnd('Proof time')
  100. console.log('Submitting withdraw transaction')
  101. await erc20mixer.methods.withdraw(proof, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 4e6 })
  102. console.log('Done')
  103. }
  104. async function getBalance(receiver) {
  105. const balance = await web3.eth.getBalance(receiver)
  106. console.log('Balance is ', web3.utils.fromWei(balance))
  107. }
  108. async function getBalanceErc20(receiver, relayer) {
  109. const balanceReceiver = await web3.eth.getBalance(receiver)
  110. const balanceRelayer = await web3.eth.getBalance(relayer)
  111. const tokenBalanceReceiver = await erc20.methods.balanceOf(receiver).call()
  112. const tokenBalanceRelayer = await erc20.methods.balanceOf(relayer).call()
  113. console.log('Receiver eth Balance is ', web3.utils.fromWei(balanceReceiver))
  114. console.log('Relayer eth Balance is ', web3.utils.fromWei(balanceRelayer))
  115. console.log('Receiver token Balance is ', web3.utils.fromWei(tokenBalanceReceiver.toString()))
  116. console.log('Relayer token Balance is ', web3.utils.fromWei(tokenBalanceRelayer.toString()))
  117. }
  118. async function withdraw(note, receiver) {
  119. // Decode hex string and restore the deposit object
  120. let buf = Buffer.from(note.slice(2), 'hex')
  121. let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
  122. const nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
  123. const paddedNullifierHash = nullifierHash.toString(16).padStart('66', '0x000000')
  124. const paddedCommitment = deposit.commitment.toString(16).padStart('66', '0x000000')
  125. // Get all deposit events from smart contract and assemble merkle tree from them
  126. console.log('Getting current state from mixer contract')
  127. const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' })
  128. const leaves = events
  129. .sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex)) // Sort events in chronological order
  130. .map(e => e.returnValues.commitment)
  131. const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
  132. // Find current commitment in the tree
  133. let depositEvent = events.find(e => e.returnValues.commitment.eq(paddedCommitment))
  134. let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex.toNumber() : -1
  135. // Validate that our data is correct
  136. const isValidRoot = await mixer.methods.isKnownRoot(await tree.root()).call()
  137. const isSpent = await mixer.methods.isSpent(paddedNullifierHash).call()
  138. assert(isValidRoot === true) // Merkle tree assembled correctly
  139. assert(isSpent === false) // The note is not spent
  140. assert(leafIndex >= 0) // Our deposit is present in the tree
  141. // Compute merkle proof of our commitment
  142. const { root, path_elements, path_index } = await tree.path(leafIndex)
  143. // Prepare circuit input
  144. const input = {
  145. // Public snark inputs
  146. root: root,
  147. nullifierHash,
  148. receiver: bigInt(receiver),
  149. relayer: bigInt(0),
  150. fee: bigInt(0),
  151. // Private snark inputs
  152. nullifier: deposit.nullifier,
  153. secret: deposit.secret,
  154. pathElements: path_elements,
  155. pathIndex: path_index,
  156. }
  157. console.log('Generating SNARK proof')
  158. console.time('Proof time')
  159. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  160. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  161. console.timeEnd('Proof time')
  162. console.log('Submitting withdraw transaction')
  163. await mixer.methods.withdraw(proof, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 4e6 })
  164. console.log('Done')
  165. }
  166. /**
  167. * Init web3, contracts, and snark
  168. */
  169. async function init() {
  170. let contractJson, erc20ContractJson, erc20mixerJson
  171. if (inBrowser) {
  172. // Initialize using injected web3 (Metamask)
  173. // To assemble web version run `npm run browserify`
  174. web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
  175. contractJson = await (await fetch('build/contracts/ETHMixer.json')).json()
  176. circuit = await (await fetch('build/circuits/withdraw.json')).json()
  177. proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
  178. MERKLE_TREE_HEIGHT = 16
  179. ETH_AMOUNT = 1e18
  180. EMPTY_ELEMENT = 1
  181. } else {
  182. // Initialize from local node
  183. web3 = new Web3('http://localhost:8545', null, { transactionConfirmationBlocks: 1 })
  184. contractJson = require('./build/contracts/ETHMixer.json')
  185. circuit = require('./build/circuits/withdraw.json')
  186. proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
  187. require('dotenv').config()
  188. MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT
  189. ETH_AMOUNT = process.env.ETH_AMOUNT
  190. EMPTY_ELEMENT = process.env.EMPTY_ELEMENT
  191. ERC20_TOKEN = process.env.ERC20_TOKEN
  192. erc20ContractJson = require('./build/contracts/ERC20Mock.json')
  193. erc20mixerJson = require('./build/contracts/ERC20Mixer.json')
  194. }
  195. groth16 = await buildGroth16()
  196. let netId = await web3.eth.net.getId()
  197. if (contractJson.networks[netId]) {
  198. const tx = await web3.eth.getTransaction(contractJson.networks[netId].transactionHash)
  199. mixer = new web3.eth.Contract(contractJson.abi, contractJson.networks[netId].address)
  200. mixer.deployedBlock = tx.blockNumber
  201. }
  202. const tx3 = await web3.eth.getTransaction(erc20mixerJson.networks[netId].transactionHash)
  203. erc20mixer = new web3.eth.Contract(erc20mixerJson.abi, erc20mixerJson.networks[netId].address)
  204. erc20mixer.deployedBlock = tx3.blockNumber
  205. if(ERC20_TOKEN === '') {
  206. erc20 = new web3.eth.Contract(erc20ContractJson.abi, erc20ContractJson.networks[netId].address)
  207. const tx2 = await web3.eth.getTransaction(erc20ContractJson.networks[netId].transactionHash)
  208. erc20.deployedBlock = tx2.blockNumber
  209. }
  210. console.log('Loaded')
  211. }
  212. // ========== CLI related stuff below ==============
  213. function printHelp(code = 0) {
  214. console.log(`Usage:
  215. Submit a deposit from default eth account and return the resulting note
  216. $ ./cli.js deposit
  217. Withdraw a note to 'receiver' account
  218. $ ./cli.js withdraw <note> <receiver>
  219. Check address balance
  220. $ ./cli.js balance <address>
  221. Example:
  222. $ ./cli.js deposit
  223. ...
  224. Your note: 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400
  225. $ ./cli.js withdraw 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400 0xee6249BA80596A4890D1BD84dbf5E4322eA4E7f0
  226. `)
  227. process.exit(code)
  228. }
  229. if (inBrowser) {
  230. window.deposit = deposit
  231. window.withdraw = async () => {
  232. const note = prompt('Enter the note to withdraw')
  233. const receiver = (await web3.eth.getAccounts())[0]
  234. await withdraw(note, receiver)
  235. }
  236. init()
  237. } else {
  238. const args = process.argv.slice(2)
  239. if (args.length === 0) {
  240. printHelp()
  241. } else {
  242. switch (args[0]) {
  243. case 'deposit':
  244. if (args.length === 1) {
  245. init().then(() => deposit()).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  246. }
  247. else
  248. printHelp(1)
  249. break
  250. case 'depositErc20':
  251. if (args.length === 1) {
  252. init().then(() => depositErc20()).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  253. }
  254. else
  255. printHelp(1)
  256. break
  257. case 'balance':
  258. if (args.length === 2 && /^0x[0-9a-fA-F]{40}$/.test(args[1])) {
  259. init().then(() => getBalance(args[1])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  260. } else
  261. printHelp(1)
  262. break
  263. case 'balanceErc20':
  264. if (args.length === 3 && /^0x[0-9a-fA-F]{40}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
  265. init().then(() => getBalanceErc20(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  266. } else
  267. printHelp(1)
  268. break
  269. case 'withdraw':
  270. if (args.length === 3 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
  271. init().then(() => withdraw(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  272. }
  273. else
  274. printHelp(1)
  275. break
  276. case 'withdrawErc20':
  277. if (args.length === 4 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2]) && /^0x[0-9a-fA-F]{40}$/.test(args[3])) {
  278. init().then(() => withdrawErc20(args[1], args[2], args[3])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  279. }
  280. else
  281. printHelp(1)
  282. break
  283. case 'test':
  284. if (args.length === 1) {
  285. (async () => {
  286. await init()
  287. const account = (await web3.eth.getAccounts())[0]
  288. const note = await deposit()
  289. await withdraw(note, account)
  290. const note2 = await deposit()
  291. await withdraw(note2, account, account)
  292. process.exit(0)
  293. })()
  294. }
  295. else
  296. printHelp(1)
  297. break
  298. default:
  299. printHelp(1)
  300. }
  301. }
  302. }