ETHMixer.test.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. /* global artifacts, web3, contract */
  2. require('chai')
  3. .use(require('bn-chai')(web3.utils.BN))
  4. .use(require('chai-as-promised'))
  5. .should()
  6. const fs = require('fs')
  7. const { toBN, toHex, randomHex } = require('web3-utils')
  8. const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper')
  9. const Mixer = artifacts.require('./ETHMixer.sol')
  10. const { ETH_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT } = process.env
  11. const websnarkUtils = require('websnark/src/utils')
  12. const buildGroth16 = require('websnark/src/groth16')
  13. const stringifyBigInts = require('websnark/tools/stringifybigint').stringifyBigInts
  14. const unstringifyBigInts2 = require('snarkjs/src/stringifybigint').unstringifyBigInts
  15. const snarkjs = require('snarkjs')
  16. const bigInt = snarkjs.bigInt
  17. const crypto = require('crypto')
  18. const circomlib = require('circomlib')
  19. const MerkleTree = require('../lib/MerkleTree')
  20. const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
  21. const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
  22. function generateDeposit() {
  23. let deposit = {
  24. secret: rbigint(31),
  25. nullifier: rbigint(31),
  26. }
  27. const preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)])
  28. deposit.commitment = pedersenHash(preimage)
  29. return deposit
  30. }
  31. // eslint-disable-next-line no-unused-vars
  32. function BNArrayToStringArray(array) {
  33. const arrayToPrint = []
  34. array.forEach(item => {
  35. arrayToPrint.push(item.toString())
  36. })
  37. return arrayToPrint
  38. }
  39. function getRandomReceiver() {
  40. let receiver = rbigint(20)
  41. while (toHex(receiver.toString()).length !== 42) {
  42. receiver = rbigint(20)
  43. }
  44. return receiver
  45. }
  46. function snarkVerify(proof) {
  47. proof = unstringifyBigInts2(proof)
  48. const verification_key = unstringifyBigInts2(require('../build/circuits/withdraw_verification_key.json'))
  49. return snarkjs['groth'].isValid(verification_key, proof, proof.publicSignals)
  50. }
  51. contract('ETHMixer', accounts => {
  52. let mixer
  53. const sender = accounts[0]
  54. const operator = accounts[0]
  55. const levels = MERKLE_TREE_HEIGHT || 16
  56. const zeroValue = EMPTY_ELEMENT || 1337
  57. const value = ETH_AMOUNT || '1000000000000000000' // 1 ether
  58. let snapshotId
  59. let prefix = 'test'
  60. let tree
  61. const fee = bigInt(ETH_AMOUNT).shr(1) || bigInt(1e17)
  62. const refund = bigInt(0)
  63. const receiver = getRandomReceiver()
  64. const relayer = accounts[1]
  65. let groth16
  66. let circuit
  67. let proving_key
  68. before(async () => {
  69. tree = new MerkleTree(
  70. levels,
  71. zeroValue,
  72. null,
  73. prefix,
  74. )
  75. mixer = await Mixer.deployed()
  76. snapshotId = await takeSnapshot()
  77. groth16 = await buildGroth16()
  78. circuit = require('../build/circuits/withdraw.json')
  79. proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
  80. })
  81. describe('#constructor', () => {
  82. it('should initialize', async () => {
  83. const etherDenomination = await mixer.denomination()
  84. etherDenomination.should.be.eq.BN(toBN(value))
  85. })
  86. })
  87. describe('#deposit', () => {
  88. it('should emit event', async () => {
  89. let commitment = 42
  90. let { logs } = await mixer.deposit(commitment, { value, from: sender })
  91. logs[0].event.should.be.equal('Deposit')
  92. logs[0].args.commitment.should.be.eq.BN(toBN(commitment))
  93. logs[0].args.leafIndex.should.be.eq.BN(toBN(0))
  94. commitment = 12;
  95. ({ logs } = await mixer.deposit(commitment, { value, from: accounts[2] }))
  96. logs[0].event.should.be.equal('Deposit')
  97. logs[0].args.commitment.should.be.eq.BN(toBN(commitment))
  98. logs[0].args.leafIndex.should.be.eq.BN(toBN(1))
  99. })
  100. it('should not deposit if disabled', async () => {
  101. let commitment = 42;
  102. (await mixer.isDepositsEnabled()).should.be.equal(true)
  103. const err = await mixer.toggleDeposits({ from: accounts[1] }).should.be.rejected
  104. err.reason.should.be.equal('Only operator can call this function.')
  105. await mixer.toggleDeposits({ from: sender });
  106. (await mixer.isDepositsEnabled()).should.be.equal(false)
  107. let error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected
  108. error.reason.should.be.equal('deposits are disabled')
  109. })
  110. it('should throw if there is a such commitment', async () => {
  111. const commitment = 42
  112. await mixer.deposit(commitment, { value, from: sender }).should.be.fulfilled
  113. const error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected
  114. error.reason.should.be.equal('The commitment has been submitted')
  115. })
  116. })
  117. describe('snark proof verification on js side', () => {
  118. it('should detect tampering', async () => {
  119. const deposit = generateDeposit()
  120. await tree.insert(deposit.commitment)
  121. const { root, path_elements, path_index } = await tree.path(0)
  122. const input = stringifyBigInts({
  123. root,
  124. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  125. nullifier: deposit.nullifier,
  126. relayer: operator,
  127. receiver,
  128. fee,
  129. refund,
  130. secret: deposit.secret,
  131. pathElements: path_elements,
  132. pathIndex: path_index,
  133. })
  134. let proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  135. const originalProof = JSON.parse(JSON.stringify(proofData))
  136. let result = snarkVerify(proofData)
  137. result.should.be.equal(true)
  138. // nullifier
  139. proofData.publicSignals[1] = '133792158246920651341275668520530514036799294649489851421007411546007850802'
  140. result = snarkVerify(proofData)
  141. result.should.be.equal(false)
  142. proofData = originalProof
  143. // try to cheat with recipient
  144. proofData.publicSignals[2] = '133738360804642228759657445999390850076318544422'
  145. result = snarkVerify(proofData)
  146. result.should.be.equal(false)
  147. proofData = originalProof
  148. // fee
  149. proofData.publicSignals[3] = '1337100000000000000000'
  150. result = snarkVerify(proofData)
  151. result.should.be.equal(false)
  152. proofData = originalProof
  153. })
  154. })
  155. describe('#withdraw', () => {
  156. it('should work', async () => {
  157. const deposit = generateDeposit()
  158. const user = accounts[4]
  159. await tree.insert(deposit.commitment)
  160. const balanceUserBefore = await web3.eth.getBalance(user)
  161. // Uncomment to measure gas usage
  162. // let gas = await mixer.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
  163. // console.log('deposit gas:', gas)
  164. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
  165. const balanceUserAfter = await web3.eth.getBalance(user)
  166. balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value)))
  167. const { root, path_elements, path_index } = await tree.path(0)
  168. // Circuit input
  169. const input = stringifyBigInts({
  170. // public
  171. root,
  172. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  173. relayer: operator,
  174. receiver,
  175. fee,
  176. refund,
  177. // private
  178. nullifier: deposit.nullifier,
  179. secret: deposit.secret,
  180. pathElements: path_elements,
  181. pathIndex: path_index,
  182. })
  183. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  184. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  185. const balanceMixerBefore = await web3.eth.getBalance(mixer.address)
  186. const balanceRelayerBefore = await web3.eth.getBalance(relayer)
  187. const balanceOperatorBefore = await web3.eth.getBalance(operator)
  188. const balanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString()))
  189. let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
  190. isSpent.should.be.equal(false)
  191. // Uncomment to measure gas usage
  192. // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
  193. // console.log('withdraw gas:', gas)
  194. const { logs } = await mixer.withdraw(proof, publicSignals, { from: relayer, gasPrice: '0' })
  195. const balanceMixerAfter = await web3.eth.getBalance(mixer.address)
  196. const balanceRelayerAfter = await web3.eth.getBalance(relayer)
  197. const balanceOperatorAfter = await web3.eth.getBalance(operator)
  198. const balanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString()))
  199. const feeBN = toBN(fee.toString())
  200. balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(value)))
  201. balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
  202. balanceOperatorAfter.should.be.eq.BN(toBN(balanceOperatorBefore).add(feeBN))
  203. balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(value)).sub(feeBN))
  204. logs[0].event.should.be.equal('Withdraw')
  205. logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
  206. logs[0].args.relayer.should.be.eq.BN(operator)
  207. logs[0].args.fee.should.be.eq.BN(feeBN)
  208. isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
  209. isSpent.should.be.equal(true)
  210. })
  211. it('should prevent double spend', async () => {
  212. const deposit = generateDeposit()
  213. await tree.insert(deposit.commitment)
  214. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  215. const { root, path_elements, path_index } = await tree.path(0)
  216. const input = stringifyBigInts({
  217. root,
  218. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  219. nullifier: deposit.nullifier,
  220. relayer: operator,
  221. receiver,
  222. fee,
  223. refund,
  224. secret: deposit.secret,
  225. pathElements: path_elements,
  226. pathIndex: path_index,
  227. })
  228. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  229. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  230. await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.fulfilled
  231. const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  232. error.reason.should.be.equal('The note has been already spent')
  233. })
  234. it('should prevent double spend with overflow', async () => {
  235. const deposit = generateDeposit()
  236. await tree.insert(deposit.commitment)
  237. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  238. const { root, path_elements, path_index } = await tree.path(0)
  239. const input = stringifyBigInts({
  240. root,
  241. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  242. nullifier: deposit.nullifier,
  243. relayer: operator,
  244. receiver,
  245. fee,
  246. refund,
  247. secret: deposit.secret,
  248. pathElements: path_elements,
  249. pathIndex: path_index,
  250. })
  251. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  252. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  253. publicSignals[1] ='0x' + toBN(publicSignals[1]).add(toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617')).toString('hex')
  254. const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  255. error.reason.should.be.equal('verifier-gte-snark-scalar-field')
  256. })
  257. it('fee should be less or equal transfer value', async () => {
  258. const deposit = generateDeposit()
  259. await tree.insert(deposit.commitment)
  260. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  261. const { root, path_elements, path_index } = await tree.path(0)
  262. const oneEtherFee = bigInt(1e18) // 1 ether
  263. const input = stringifyBigInts({
  264. root,
  265. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  266. nullifier: deposit.nullifier,
  267. relayer: operator,
  268. receiver,
  269. fee: oneEtherFee,
  270. refund,
  271. secret: deposit.secret,
  272. pathElements: path_elements,
  273. pathIndex: path_index,
  274. })
  275. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  276. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  277. const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  278. error.reason.should.be.equal('Fee exceeds transfer value')
  279. })
  280. it('should throw for corrupted merkle tree root', async () => {
  281. const deposit = generateDeposit()
  282. await tree.insert(deposit.commitment)
  283. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  284. const { root, path_elements, path_index } = await tree.path(0)
  285. const input = stringifyBigInts({
  286. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  287. root,
  288. nullifier: deposit.nullifier,
  289. relayer: operator,
  290. receiver,
  291. fee,
  292. refund,
  293. secret: deposit.secret,
  294. pathElements: path_elements,
  295. pathIndex: path_index,
  296. })
  297. const dummyRoot = randomHex(32)
  298. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  299. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  300. publicSignals[0] = dummyRoot
  301. const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  302. error.reason.should.be.equal('Cannot find your merkle root')
  303. })
  304. it('should reject with tampered public inputs', async () => {
  305. const deposit = generateDeposit()
  306. await tree.insert(deposit.commitment)
  307. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  308. let { root, path_elements, path_index } = await tree.path(0)
  309. const input = stringifyBigInts({
  310. root,
  311. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  312. nullifier: deposit.nullifier,
  313. relayer: operator,
  314. receiver,
  315. fee,
  316. refund,
  317. secret: deposit.secret,
  318. pathElements: path_elements,
  319. pathIndex: path_index,
  320. })
  321. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  322. let { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  323. const originalPublicSignals = publicSignals.slice()
  324. const originalProof = proof.slice()
  325. // receiver
  326. publicSignals[2] = '0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337'
  327. let error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  328. error.reason.should.be.equal('Invalid withdraw proof')
  329. // fee
  330. publicSignals = originalPublicSignals.slice()
  331. publicSignals[3] = '0x000000000000000000000000000000000000000000000000015345785d8a0000'
  332. error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  333. error.reason.should.be.equal('Invalid withdraw proof')
  334. // nullifier
  335. publicSignals = originalPublicSignals.slice()
  336. publicSignals[1] = '0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'
  337. error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  338. error.reason.should.be.equal('Invalid withdraw proof')
  339. // proof itself
  340. proof[0] = '0x261d81d8203437f29b38a88c4263476d858e6d9645cf21740461684412b31337'
  341. await mixer.withdraw(proof, originalPublicSignals, { from: relayer }).should.be.rejected
  342. // should work with original values
  343. await mixer.withdraw(originalProof, originalPublicSignals, { from: relayer }).should.be.fulfilled
  344. })
  345. it('should reject with non zero refund', async () => {
  346. const deposit = generateDeposit()
  347. await tree.insert(deposit.commitment)
  348. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  349. const { root, path_elements, path_index } = await tree.path(0)
  350. const input = stringifyBigInts({
  351. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  352. root,
  353. nullifier: deposit.nullifier,
  354. relayer: operator,
  355. receiver,
  356. fee,
  357. refund: bigInt(1),
  358. secret: deposit.secret,
  359. pathElements: path_elements,
  360. pathIndex: path_index,
  361. })
  362. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  363. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  364. const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  365. error.reason.should.be.equal('Refund value is supposed to be zero for ETH mixer')
  366. })
  367. })
  368. describe('#changeOperator', () => {
  369. it('should work', async () => {
  370. let operator = await mixer.operator()
  371. operator.should.be.equal(sender)
  372. const newOperator = accounts[7]
  373. await mixer.changeOperator(newOperator).should.be.fulfilled
  374. operator = await mixer.operator()
  375. operator.should.be.equal(newOperator)
  376. })
  377. it('cannot change from different address', async () => {
  378. let operator = await mixer.operator()
  379. operator.should.be.equal(sender)
  380. const newOperator = accounts[7]
  381. const error = await mixer.changeOperator(newOperator, { from: accounts[7] }).should.be.rejected
  382. error.reason.should.be.equal('Only operator can call this function.')
  383. })
  384. })
  385. describe('#updateVerifier', () => {
  386. it('should work', async () => {
  387. let operator = await mixer.operator()
  388. operator.should.be.equal(sender)
  389. const newVerifier = accounts[7]
  390. await mixer.updateVerifier(newVerifier).should.be.fulfilled
  391. const verifier = await mixer.verifier()
  392. verifier.should.be.equal(newVerifier)
  393. })
  394. it('cannot change from different address', async () => {
  395. let operator = await mixer.operator()
  396. operator.should.be.equal(sender)
  397. const newVerifier = accounts[7]
  398. const error = await mixer.updateVerifier(newVerifier, { from: accounts[7] }).should.be.rejected
  399. error.reason.should.be.equal('Only operator can call this function.')
  400. })
  401. })
  402. describe('#disableVerifierUpdate', () => {
  403. it('should work', async () => {
  404. let operator = await mixer.operator()
  405. operator.should.be.equal(sender)
  406. let isVerifierUpdateAllowed = await mixer.isVerifierUpdateAllowed()
  407. isVerifierUpdateAllowed.should.be.equal(true)
  408. await mixer.disableVerifierUpdate().should.be.fulfilled
  409. const newValue = await mixer.isVerifierUpdateAllowed()
  410. newValue.should.be.equal(false)
  411. })
  412. it('cannot update verifier after this function is called', async () => {
  413. let operator = await mixer.operator()
  414. operator.should.be.equal(sender)
  415. let isVerifierUpdateAllowed = await mixer.isVerifierUpdateAllowed()
  416. isVerifierUpdateAllowed.should.be.equal(true)
  417. await mixer.disableVerifierUpdate().should.be.fulfilled
  418. const newValue = await mixer.isVerifierUpdateAllowed()
  419. newValue.should.be.equal(false)
  420. const newVerifier = accounts[7]
  421. const error = await mixer.updateVerifier(newVerifier).should.be.rejected
  422. error.reason.should.be.equal('Verifier updates have been disabled.')
  423. })
  424. })
  425. afterEach(async () => {
  426. await revertSnapshot(snapshotId.result)
  427. // eslint-disable-next-line require-atomic-updates
  428. snapshotId = await takeSnapshot()
  429. tree = new MerkleTree(
  430. levels,
  431. zeroValue,
  432. null,
  433. prefix,
  434. )
  435. })
  436. })