cli.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  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. const { GSNProvider, GSNDevProvider } = require('@openzeppelin/gsn-provider')
  15. const { ephemeral } = require('@openzeppelin/network')
  16. let web3, mixer, erc20mixer, circuit, proving_key, groth16, erc20
  17. let MERKLE_TREE_HEIGHT, ETH_AMOUNT, EMPTY_ELEMENT, ERC20_TOKEN
  18. const inBrowser = (typeof window !== 'undefined')
  19. /** Generate random number of specified byte length */
  20. const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
  21. /** Compute pedersen hash */
  22. const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
  23. /**
  24. * Create deposit object from secret and nullifier
  25. */
  26. function createDeposit(nullifier, secret) {
  27. let deposit = { nullifier, secret }
  28. deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
  29. deposit.commitment = pedersenHash(deposit.preimage)
  30. deposit.nullifierHash = pedersenHash(nullifier.leInt2Buff(31))
  31. return deposit
  32. }
  33. /**
  34. * Make a deposit
  35. * @returns {Promise<string>}
  36. */
  37. async function deposit() {
  38. const deposit = createDeposit(rbigint(31), rbigint(31))
  39. const fromAccount = (await web3.eth.getAccounts())[0]
  40. console.log('Submitting deposit transaction')
  41. await mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ value: ETH_AMOUNT, from: fromAccount, gas:1e6 })
  42. const note = '0x' + deposit.preimage.toString('hex')
  43. console.log('Your note:', note)
  44. return note
  45. }
  46. async function depositErc20() {
  47. const fromAccount = (await web3.eth.getAccounts())[0]
  48. const tokenAmount = process.env.TOKEN_AMOUNT
  49. await erc20.methods.mint(fromAccount, tokenAmount).send({ from: fromAccount, gas:1e6 })
  50. await erc20.methods.approve(erc20mixer.address, tokenAmount).send({ from: fromAccount, gas:1e6 })
  51. const allowance = await erc20.methods.allowance(fromAccount, erc20mixer.address).call()
  52. console.log('erc20mixer allowance', allowance.toString(10))
  53. const deposit = createDeposit(rbigint(31), rbigint(31))
  54. await erc20mixer.methods.deposit('0x' + deposit.commitment.toString(16)).send({ from: fromAccount, gas:1e6 })
  55. const balance = await erc20.methods.balanceOf(erc20mixer.address).call()
  56. console.log('erc20mixer balance', balance.toString(10))
  57. const note = '0x' + deposit.preimage.toString('hex')
  58. console.log('Your note:', note)
  59. return note
  60. }
  61. async function withdrawErc20(note, receiver, relayer) {
  62. let buf = Buffer.from(note.slice(2), 'hex')
  63. let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
  64. console.log('Getting current state from mixer contract')
  65. const events = await erc20mixer.getPastEvents('Deposit', { fromBlock: erc20mixer.deployedBlock, toBlock: 'latest' })
  66. let leafIndex
  67. const commitment = deposit.commitment.toString(16).padStart('66', '0x000000')
  68. const leaves = events
  69. .sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex))
  70. .map(e => {
  71. if (e.returnValues.commitment.eq(commitment)) {
  72. leafIndex = e.returnValues.leafIndex.toNumber()
  73. }
  74. return e.returnValues.commitment
  75. })
  76. const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
  77. const validRoot = await erc20mixer.methods.isKnownRoot(await tree.root()).call()
  78. const nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
  79. const nullifierHashToCheck = nullifierHash.toString(16).padStart('66', '0x000000')
  80. const isSpent = await erc20mixer.methods.isSpent(nullifierHashToCheck).call()
  81. assert(validRoot === true)
  82. assert(isSpent === false)
  83. assert(leafIndex >= 0)
  84. const { root, path_elements, path_index } = await tree.path(leafIndex)
  85. // Circuit input
  86. const input = {
  87. // public
  88. root: root,
  89. nullifierHash,
  90. receiver: bigInt(receiver),
  91. relayer: bigInt(relayer),
  92. fee: bigInt(web3.utils.toWei('0.01')),
  93. refund: bigInt(0),
  94. // private
  95. nullifier: deposit.nullifier,
  96. secret: deposit.secret,
  97. pathElements: path_elements,
  98. pathIndex: path_index,
  99. }
  100. console.log('Generating SNARK proof')
  101. console.time('Proof time')
  102. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  103. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  104. console.timeEnd('Proof time')
  105. console.log('Submitting withdraw transaction')
  106. await erc20mixer.methods.withdraw(proof, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 })
  107. console.log('Done')
  108. }
  109. async function getBalance(receiver) {
  110. const balance = await web3.eth.getBalance(receiver)
  111. console.log('Balance is ', web3.utils.fromWei(balance))
  112. }
  113. async function getBalanceErc20(receiver, relayer) {
  114. const balanceReceiver = await web3.eth.getBalance(receiver)
  115. const balanceRelayer = await web3.eth.getBalance(relayer)
  116. const tokenBalanceReceiver = await erc20.methods.balanceOf(receiver).call()
  117. const tokenBalanceRelayer = await erc20.methods.balanceOf(relayer).call()
  118. console.log('Receiver eth Balance is ', web3.utils.fromWei(balanceReceiver))
  119. console.log('Relayer eth Balance is ', web3.utils.fromWei(balanceRelayer))
  120. console.log('Receiver token Balance is ', web3.utils.fromWei(tokenBalanceReceiver.toString()))
  121. console.log('Relayer token Balance is ', web3.utils.fromWei(tokenBalanceRelayer.toString()))
  122. }
  123. async function withdraw(note, receiver) {
  124. // Decode hex string and restore the deposit object
  125. let buf = Buffer.from(note.slice(2), 'hex')
  126. let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
  127. const nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
  128. const paddedNullifierHash = nullifierHash.toString(16).padStart('66', '0x000000')
  129. const paddedCommitment = deposit.commitment.toString(16).padStart('66', '0x000000')
  130. // Get all deposit events from smart contract and assemble merkle tree from them
  131. console.log('Getting current state from mixer contract')
  132. const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' })
  133. const leaves = events
  134. .sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex)) // Sort events in chronological order
  135. .map(e => e.returnValues.commitment)
  136. const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
  137. // Find current commitment in the tree
  138. let depositEvent = events.find(e => e.returnValues.commitment.eq(paddedCommitment))
  139. let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex.toNumber() : -1
  140. // Validate that our data is correct
  141. const isValidRoot = await mixer.methods.isKnownRoot(await tree.root()).call()
  142. const isSpent = await mixer.methods.isSpent(paddedNullifierHash).call()
  143. assert(isValidRoot === true) // Merkle tree assembled correctly
  144. assert(isSpent === false) // The note is not spent
  145. assert(leafIndex >= 0) // Our deposit is present in the tree
  146. // Compute merkle proof of our commitment
  147. const { root, path_elements, path_index } = await tree.path(leafIndex)
  148. // Prepare circuit input
  149. const input = {
  150. // Public snark inputs
  151. root: root,
  152. nullifierHash,
  153. receiver: bigInt(receiver),
  154. relayer: bigInt(0),
  155. fee: bigInt(0),
  156. refund: bigInt(0),
  157. // Private snark inputs
  158. nullifier: deposit.nullifier,
  159. secret: deposit.secret,
  160. pathElements: path_elements,
  161. pathIndex: path_index,
  162. }
  163. console.log('Generating SNARK proof')
  164. console.time('Proof time')
  165. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  166. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  167. console.timeEnd('Proof time')
  168. console.log('Submitting withdraw transaction')
  169. await mixer.methods.withdraw(proof, publicSignals).send({ from: (await web3.eth.getAccounts())[0], gas: 1e6 })
  170. console.log('Done')
  171. }
  172. async function buildDepositTree() {
  173. // Get all deposit events from smart contract and assemble merkle tree from them
  174. console.log('Getting current state from mixer contract')
  175. const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' })
  176. const leaves = events
  177. .sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex)) // Sort events in chronological order
  178. .map(e => e.returnValues.commitment)
  179. const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
  180. }
  181. async function withdrawViaRelayer(note, receiver) {
  182. // Decode hex string and restore the deposit object
  183. let buf = Buffer.from(note.slice(2), 'hex')
  184. let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
  185. const nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
  186. const paddedNullifierHash = nullifierHash.toString(16).padStart('66', '0x000000')
  187. const paddedCommitment = deposit.commitment.toString(16).padStart('66', '0x000000')
  188. // Get all deposit events from smart contract and assemble merkle tree from them
  189. console.log('Getting current state from mixer contract')
  190. const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' })
  191. const leaves = events
  192. .sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex)) // Sort events in chronological order
  193. .map(e => e.returnValues.commitment)
  194. const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
  195. // Find current commitment in the tree
  196. let depositEvent = events.find(e => e.returnValues.commitment.eq(paddedCommitment))
  197. let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex.toNumber() : -1
  198. // Validate that our data is correct
  199. const isValidRoot = await mixer.methods.isKnownRoot(await tree.root()).call()
  200. const isSpent = await mixer.methods.isSpent(paddedNullifierHash).call()
  201. assert(isValidRoot === true, 'Merkle tree assembled incorrectly') // Merkle tree assembled correctly
  202. assert(isSpent === false, 'The note is spent') // The note is not spent
  203. assert(leafIndex >= 0, 'Our deposit is not present in the tree') // Our deposit is present in the tree
  204. // Compute merkle proof of our commitment
  205. const { root, path_elements, path_index } = await tree.path(leafIndex)
  206. // Prepare circuit input
  207. const input = {
  208. // Public snark inputs
  209. root: root,
  210. nullifierHash,
  211. receiver: bigInt(receiver),
  212. relayer: bigInt(0),
  213. fee: bigInt(web3.utils.toWei('0.01')),
  214. refund: bigInt(0),
  215. // Private snark inputs
  216. nullifier: deposit.nullifier,
  217. secret: deposit.secret,
  218. pathElements: path_elements,
  219. pathIndex: path_index,
  220. }
  221. console.log('Generating SNARK proof')
  222. console.time('Proof time')
  223. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  224. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  225. console.timeEnd('Proof time')
  226. console.log('Submitting withdraw transaction via relayer')
  227. const account = ephemeral()
  228. const HARDCODED_RELAYER_OPTS = {
  229. txFee: 90,
  230. fixedGasPrice: 22000000001,
  231. gasPrice: 22000000001,
  232. fixedGasLimit: 5000000,
  233. gasLimit: 5000000,
  234. verbose: true,
  235. }
  236. const provider = new GSNProvider('https://rinkeby.infura.io/v3/c7463beadf2144e68646ff049917b716', { signKey: account })
  237. // const provider = new GSNDevProvider('http://localhost:8545', { signKey: account, HARDCODED_RELAYER_OPTS })
  238. web3 = new Web3(provider)
  239. const netId = await web3.eth.net.getId()
  240. console.log('netId', netId)
  241. // eslint-disable-next-line require-atomic-updates
  242. mixer = new web3.eth.Contract(contractJson.abi, contractJson.networks[netId].address)
  243. console.log('mixer address', contractJson.networks[netId].address)
  244. const tx = await mixer.methods.withdrawViaRelayer(proof, publicSignals).send({ from: account.address, gas: 2e6 })
  245. console.log('tx', tx)
  246. console.log('Done')
  247. }
  248. /**
  249. * Init web3, contracts, and snark
  250. */
  251. async function init() {
  252. let contractJson, erc20ContractJson, erc20mixerJson
  253. if (inBrowser) {
  254. // Initialize using injected web3 (Metamask)
  255. // To assemble web version run `npm run browserify`
  256. web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 })
  257. contractJson = await (await fetch('build/contracts/ETHMixer.json')).json()
  258. circuit = await (await fetch('build/circuits/withdraw.json')).json()
  259. proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
  260. MERKLE_TREE_HEIGHT = 16
  261. ETH_AMOUNT = 1e18
  262. EMPTY_ELEMENT = 1
  263. } else {
  264. // Initialize from local node
  265. web3 = new Web3('http://localhost:8545', null, { transactionConfirmationBlocks: 1 })
  266. contractJson = require('./build/contracts/ETHMixer.json')
  267. circuit = require('./build/circuits/withdraw.json')
  268. proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
  269. require('dotenv').config()
  270. MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT
  271. ETH_AMOUNT = process.env.ETH_AMOUNT
  272. EMPTY_ELEMENT = process.env.EMPTY_ELEMENT
  273. ERC20_TOKEN = process.env.ERC20_TOKEN
  274. erc20ContractJson = require('./build/contracts/ERC20Mock.json')
  275. erc20mixerJson = require('./build/contracts/ERC20Mixer.json')
  276. }
  277. groth16 = await buildGroth16()
  278. let netId = await web3.eth.net.getId()
  279. if (contractJson.networks[netId]) {
  280. const tx = await web3.eth.getTransaction(contractJson.networks[netId].transactionHash)
  281. mixer = new web3.eth.Contract(contractJson.abi, contractJson.networks[netId].address)
  282. mixer.deployedBlock = tx.blockNumber
  283. }
  284. const tx3 = await web3.eth.getTransaction(erc20mixerJson.networks[netId].transactionHash)
  285. erc20mixer = new web3.eth.Contract(erc20mixerJson.abi, erc20mixerJson.networks[netId].address)
  286. erc20mixer.deployedBlock = tx3.blockNumber
  287. if(ERC20_TOKEN === '') {
  288. erc20 = new web3.eth.Contract(erc20ContractJson.abi, erc20ContractJson.networks[netId].address)
  289. const tx2 = await web3.eth.getTransaction(erc20ContractJson.networks[netId].transactionHash)
  290. erc20.deployedBlock = tx2.blockNumber
  291. }
  292. console.log('Loaded')
  293. }
  294. // ========== CLI related stuff below ==============
  295. function printHelp(code = 0) {
  296. console.log(`Usage:
  297. Submit a deposit from default eth account and return the resulting note
  298. $ ./cli.js deposit
  299. Withdraw a note to 'receiver' account
  300. $ ./cli.js withdraw <note> <receiver>
  301. Check address balance
  302. $ ./cli.js balance <address>
  303. Example:
  304. $ ./cli.js deposit
  305. ...
  306. Your note: 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400
  307. $ ./cli.js withdraw 0x1941fa999e2b4bfeec3ce53c2440c3bc991b1b84c9bb650ea19f8331baf621001e696487e2a2ee54541fa12f49498d71e24d00b1731a8ccd4f5f5126f3d9f400 0xee6249BA80596A4890D1BD84dbf5E4322eA4E7f0
  308. `)
  309. process.exit(code)
  310. }
  311. if (inBrowser) {
  312. window.deposit = deposit
  313. window.withdraw = async () => {
  314. const note = prompt('Enter the note to withdraw')
  315. const receiver = (await web3.eth.getAccounts())[0]
  316. await withdraw(note, receiver)
  317. }
  318. window.withdrawViaRelayer = async () => {
  319. const note = prompt('Enter the note to withdrawViaRelayer')
  320. const receiver = (await web3.eth.getAccounts())[0]
  321. await withdrawViaRelayer(note, receiver)
  322. }
  323. init()
  324. } else {
  325. const args = process.argv.slice(2)
  326. if (args.length === 0) {
  327. printHelp()
  328. } else {
  329. switch (args[0]) {
  330. case 'deposit':
  331. if (args.length === 1) {
  332. init().then(() => deposit()).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  333. }
  334. else
  335. printHelp(1)
  336. break
  337. case 'depositErc20':
  338. if (args.length === 1) {
  339. init().then(() => depositErc20()).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  340. }
  341. else
  342. printHelp(1)
  343. break
  344. case 'balance':
  345. if (args.length === 2 && /^0x[0-9a-fA-F]{40}$/.test(args[1])) {
  346. init().then(() => getBalance(args[1])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  347. } else
  348. printHelp(1)
  349. break
  350. case 'balanceErc20':
  351. if (args.length === 3 && /^0x[0-9a-fA-F]{40}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
  352. init().then(() => getBalanceErc20(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  353. } else
  354. printHelp(1)
  355. break
  356. case 'withdraw':
  357. if (args.length === 3 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
  358. init().then(() => withdraw(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  359. }
  360. else
  361. printHelp(1)
  362. break
  363. case 'withdrawErc20':
  364. 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])) {
  365. init().then(() => withdrawErc20(args[1], args[2], args[3])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  366. }
  367. else
  368. printHelp(1)
  369. break
  370. case 'withdrawViaRelayer':
  371. if (args.length === 3 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
  372. init().then(() => withdrawViaRelayer(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
  373. }
  374. else
  375. printHelp(1)
  376. break
  377. case 'test':
  378. if (args.length === 1) {
  379. (async () => {
  380. await init()
  381. const account = (await web3.eth.getAccounts())[0]
  382. const note = await deposit()
  383. await withdraw(note, account)
  384. const note2 = await deposit()
  385. await withdraw(note2, account, account)
  386. process.exit(0)
  387. })()
  388. }
  389. else
  390. printHelp(1)
  391. break
  392. default:
  393. printHelp(1)
  394. }
  395. }
  396. }