ETHMixer.test.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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 receiver = getRandomReceiver()
  63. const relayer = accounts[1]
  64. let groth16
  65. let circuit
  66. let proving_key
  67. before(async () => {
  68. tree = new MerkleTree(
  69. levels,
  70. zeroValue,
  71. null,
  72. prefix,
  73. )
  74. mixer = await Mixer.deployed()
  75. snapshotId = await takeSnapshot()
  76. groth16 = await buildGroth16()
  77. circuit = require('../build/circuits/withdraw.json')
  78. proving_key = fs.readFileSync('build/circuits/withdraw_proving_key.bin').buffer
  79. })
  80. describe('#constructor', () => {
  81. it('should initialize', async () => {
  82. const etherDenomination = await mixer.denomination()
  83. etherDenomination.should.be.eq.BN(toBN(value))
  84. })
  85. })
  86. describe('#deposit', () => {
  87. it('should emit event', async () => {
  88. let commitment = 42
  89. let { logs } = await mixer.deposit(commitment, { value, from: sender })
  90. logs[0].event.should.be.equal('Deposit')
  91. logs[0].args.commitment.should.be.eq.BN(toBN(commitment))
  92. logs[0].args.leafIndex.should.be.eq.BN(toBN(0))
  93. commitment = 12;
  94. ({ logs } = await mixer.deposit(commitment, { value, from: accounts[2] }))
  95. logs[0].event.should.be.equal('Deposit')
  96. logs[0].args.commitment.should.be.eq.BN(toBN(commitment))
  97. logs[0].args.leafIndex.should.be.eq.BN(toBN(1))
  98. })
  99. it('should not deposit if disabled', async () => {
  100. let commitment = 42;
  101. (await mixer.isDepositsEnabled()).should.be.equal(true)
  102. const err = await mixer.toggleDeposits({ from: accounts[1] }).should.be.rejected
  103. err.reason.should.be.equal('Only operator can call this function.')
  104. await mixer.toggleDeposits({ from: sender });
  105. (await mixer.isDepositsEnabled()).should.be.equal(false)
  106. let error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected
  107. error.reason.should.be.equal('deposits are disabled')
  108. })
  109. it('should throw if there is a such commitment', async () => {
  110. const commitment = 42
  111. await mixer.deposit(commitment, { value, from: sender }).should.be.fulfilled
  112. const error = await mixer.deposit(commitment, { value, from: sender }).should.be.rejected
  113. error.reason.should.be.equal('The commitment has been submitted')
  114. })
  115. })
  116. describe('snark proof verification on js side', () => {
  117. it('should detect tampering', async () => {
  118. const deposit = generateDeposit()
  119. await tree.insert(deposit.commitment)
  120. const { root, path_elements, path_index } = await tree.path(0)
  121. const input = stringifyBigInts({
  122. root,
  123. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  124. nullifier: deposit.nullifier,
  125. relayer: operator,
  126. receiver,
  127. fee,
  128. secret: deposit.secret,
  129. pathElements: path_elements,
  130. pathIndex: path_index,
  131. })
  132. let proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  133. const originalProof = JSON.parse(JSON.stringify(proofData))
  134. let result = snarkVerify(proofData)
  135. result.should.be.equal(true)
  136. // nullifier
  137. proofData.publicSignals[1] = '133792158246920651341275668520530514036799294649489851421007411546007850802'
  138. result = snarkVerify(proofData)
  139. result.should.be.equal(false)
  140. proofData = originalProof
  141. // try to cheat with recipient
  142. proofData.publicSignals[2] = '133738360804642228759657445999390850076318544422'
  143. result = snarkVerify(proofData)
  144. result.should.be.equal(false)
  145. proofData = originalProof
  146. // fee
  147. proofData.publicSignals[3] = '1337100000000000000000'
  148. result = snarkVerify(proofData)
  149. result.should.be.equal(false)
  150. proofData = originalProof
  151. })
  152. })
  153. describe('#withdraw', () => {
  154. it('should work', async () => {
  155. const deposit = generateDeposit()
  156. const user = accounts[4]
  157. await tree.insert(deposit.commitment)
  158. const balanceUserBefore = await web3.eth.getBalance(user)
  159. // Uncomment to measure gas usage
  160. // let gas = await mixer.deposit.estimateGas(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
  161. // console.log('deposit gas:', gas)
  162. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: user, gasPrice: '0' })
  163. const balanceUserAfter = await web3.eth.getBalance(user)
  164. balanceUserAfter.should.be.eq.BN(toBN(balanceUserBefore).sub(toBN(value)))
  165. const { root, path_elements, path_index } = await tree.path(0)
  166. // Circuit input
  167. const input = stringifyBigInts({
  168. // public
  169. root,
  170. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  171. relayer: operator,
  172. receiver,
  173. fee,
  174. // private
  175. nullifier: deposit.nullifier,
  176. secret: deposit.secret,
  177. pathElements: path_elements,
  178. pathIndex: path_index,
  179. })
  180. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  181. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  182. const balanceMixerBefore = await web3.eth.getBalance(mixer.address)
  183. const balanceRelayerBefore = await web3.eth.getBalance(relayer)
  184. const balanceOperatorBefore = await web3.eth.getBalance(operator)
  185. const balanceRecieverBefore = await web3.eth.getBalance(toHex(receiver.toString()))
  186. let isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
  187. isSpent.should.be.equal(false)
  188. // Uncomment to measure gas usage
  189. // gas = await mixer.withdraw.estimateGas(proof, publicSignals, { from: relayer, gasPrice: '0' })
  190. // console.log('withdraw gas:', gas)
  191. const { logs } = await mixer.withdraw(proof, publicSignals, { from: relayer, gasPrice: '0' })
  192. const balanceMixerAfter = await web3.eth.getBalance(mixer.address)
  193. const balanceRelayerAfter = await web3.eth.getBalance(relayer)
  194. const balanceOperatorAfter = await web3.eth.getBalance(operator)
  195. const balanceRecieverAfter = await web3.eth.getBalance(toHex(receiver.toString()))
  196. const feeBN = toBN(fee.toString())
  197. balanceMixerAfter.should.be.eq.BN(toBN(balanceMixerBefore).sub(toBN(value)))
  198. balanceRelayerAfter.should.be.eq.BN(toBN(balanceRelayerBefore))
  199. balanceOperatorAfter.should.be.eq.BN(toBN(balanceOperatorBefore).add(feeBN))
  200. balanceRecieverAfter.should.be.eq.BN(toBN(balanceRecieverBefore).add(toBN(value)).sub(feeBN))
  201. logs[0].event.should.be.equal('Withdraw')
  202. logs[0].args.nullifierHash.should.be.eq.BN(toBN(input.nullifierHash.toString()))
  203. logs[0].args.relayer.should.be.eq.BN(operator)
  204. logs[0].args.fee.should.be.eq.BN(feeBN)
  205. isSpent = await mixer.isSpent(input.nullifierHash.toString(16).padStart(66, '0x00000'))
  206. isSpent.should.be.equal(true)
  207. })
  208. it('should prevent double spend', async () => {
  209. const deposit = generateDeposit()
  210. await tree.insert(deposit.commitment)
  211. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  212. const { root, path_elements, path_index } = await tree.path(0)
  213. const input = stringifyBigInts({
  214. root,
  215. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  216. nullifier: deposit.nullifier,
  217. relayer: operator,
  218. receiver,
  219. fee,
  220. secret: deposit.secret,
  221. pathElements: path_elements,
  222. pathIndex: path_index,
  223. })
  224. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  225. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  226. await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.fulfilled
  227. const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  228. error.reason.should.be.equal('The note has been already spent')
  229. })
  230. it('should prevent double spend with overflow', async () => {
  231. const deposit = generateDeposit()
  232. await tree.insert(deposit.commitment)
  233. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  234. const { root, path_elements, path_index } = await tree.path(0)
  235. const input = stringifyBigInts({
  236. root,
  237. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  238. nullifier: deposit.nullifier,
  239. relayer: operator,
  240. receiver,
  241. fee,
  242. secret: deposit.secret,
  243. pathElements: path_elements,
  244. pathIndex: path_index,
  245. })
  246. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  247. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  248. publicSignals[1] ='0x' + toBN(publicSignals[1]).add(toBN('21888242871839275222246405745257275088548364400416034343698204186575808495617')).toString('hex')
  249. const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  250. error.reason.should.be.equal('verifier-gte-snark-scalar-field')
  251. })
  252. it('fee should be less or equal transfer value', async () => {
  253. const deposit = generateDeposit()
  254. await tree.insert(deposit.commitment)
  255. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  256. const { root, path_elements, path_index } = await tree.path(0)
  257. const oneEtherFee = bigInt(1e18) // 1 ether
  258. const input = stringifyBigInts({
  259. root,
  260. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  261. nullifier: deposit.nullifier,
  262. relayer: operator,
  263. receiver,
  264. fee: oneEtherFee,
  265. secret: deposit.secret,
  266. pathElements: path_elements,
  267. pathIndex: path_index,
  268. })
  269. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  270. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  271. const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  272. error.reason.should.be.equal('Fee exceeds transfer value')
  273. })
  274. it('should throw for corrupted merkle tree root', async () => {
  275. const deposit = generateDeposit()
  276. await tree.insert(deposit.commitment)
  277. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  278. const { root, path_elements, path_index } = await tree.path(0)
  279. const input = stringifyBigInts({
  280. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  281. root,
  282. nullifier: deposit.nullifier,
  283. relayer: operator,
  284. receiver,
  285. fee,
  286. secret: deposit.secret,
  287. pathElements: path_elements,
  288. pathIndex: path_index,
  289. })
  290. const dummyRoot = randomHex(32)
  291. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  292. const { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  293. publicSignals[0] = dummyRoot
  294. const error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  295. error.reason.should.be.equal('Cannot find your merkle root')
  296. })
  297. it('should reject with tampered public inputs', async () => {
  298. const deposit = generateDeposit()
  299. await tree.insert(deposit.commitment)
  300. await mixer.deposit(toBN(deposit.commitment.toString()), { value, from: sender })
  301. let { root, path_elements, path_index } = await tree.path(0)
  302. const input = stringifyBigInts({
  303. root,
  304. nullifierHash: pedersenHash(deposit.nullifier.leInt2Buff(31)),
  305. nullifier: deposit.nullifier,
  306. relayer: operator,
  307. receiver,
  308. fee,
  309. secret: deposit.secret,
  310. pathElements: path_elements,
  311. pathIndex: path_index,
  312. })
  313. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
  314. let { proof, publicSignals } = websnarkUtils.toSolidityInput(proofData)
  315. const originalPublicSignals = publicSignals.slice()
  316. const originalProof = proof.slice()
  317. // receiver
  318. publicSignals[2] = '0x0000000000000000000000007a1f9131357404ef86d7c38dbffed2da70321337'
  319. let error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  320. error.reason.should.be.equal('Invalid withdraw proof')
  321. // fee
  322. publicSignals = originalPublicSignals.slice()
  323. publicSignals[3] = '0x000000000000000000000000000000000000000000000000015345785d8a0000'
  324. error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  325. error.reason.should.be.equal('Invalid withdraw proof')
  326. // nullifier
  327. publicSignals = originalPublicSignals.slice()
  328. publicSignals[1] = '0x00abdfc78211f8807b9c6504a6e537e71b8788b2f529a95f1399ce124a8642ad'
  329. error = await mixer.withdraw(proof, publicSignals, { from: relayer }).should.be.rejected
  330. error.reason.should.be.equal('Invalid withdraw proof')
  331. // proof itself
  332. proof[0] = '0x261d81d8203437f29b38a88c4263476d858e6d9645cf21740461684412b31337'
  333. await mixer.withdraw(proof, originalPublicSignals, { from: relayer }).should.be.rejected
  334. // should work with original values
  335. await mixer.withdraw(originalProof, originalPublicSignals, { from: relayer }).should.be.fulfilled
  336. })
  337. })
  338. describe('#changeOperator', () => {
  339. it('should work', async () => {
  340. let operator = await mixer.operator()
  341. operator.should.be.equal(sender)
  342. const newOperator = accounts[7]
  343. await mixer.changeOperator(newOperator).should.be.fulfilled
  344. operator = await mixer.operator()
  345. operator.should.be.equal(newOperator)
  346. })
  347. it('cannot change from different address', async () => {
  348. let operator = await mixer.operator()
  349. operator.should.be.equal(sender)
  350. const newOperator = accounts[7]
  351. const error = await mixer.changeOperator(newOperator, { from: accounts[7] }).should.be.rejected
  352. error.reason.should.be.equal('Only operator can call this function.')
  353. })
  354. })
  355. describe('#updateVerifier', () => {
  356. it('should work', async () => {
  357. let operator = await mixer.operator()
  358. operator.should.be.equal(sender)
  359. const newVerifier = accounts[7]
  360. await mixer.updateVerifier(newVerifier).should.be.fulfilled
  361. const verifier = await mixer.verifier()
  362. verifier.should.be.equal(newVerifier)
  363. })
  364. it('cannot change from different address', async () => {
  365. let operator = await mixer.operator()
  366. operator.should.be.equal(sender)
  367. const newVerifier = accounts[7]
  368. const error = await mixer.updateVerifier(newVerifier, { from: accounts[7] }).should.be.rejected
  369. error.reason.should.be.equal('Only operator can call this function.')
  370. })
  371. })
  372. describe('#disableVerifierUpdate', () => {
  373. it('should work', async () => {
  374. let operator = await mixer.operator()
  375. operator.should.be.equal(sender)
  376. let isVerifierUpdateAllowed = await mixer.isVerifierUpdateAllowed()
  377. isVerifierUpdateAllowed.should.be.equal(true)
  378. await mixer.disableVerifierUpdate().should.be.fulfilled
  379. const newValue = await mixer.isVerifierUpdateAllowed()
  380. newValue.should.be.equal(false)
  381. })
  382. it('cannot update verifier after this function is called', async () => {
  383. let operator = await mixer.operator()
  384. operator.should.be.equal(sender)
  385. let isVerifierUpdateAllowed = await mixer.isVerifierUpdateAllowed()
  386. isVerifierUpdateAllowed.should.be.equal(true)
  387. await mixer.disableVerifierUpdate().should.be.fulfilled
  388. const newValue = await mixer.isVerifierUpdateAllowed()
  389. newValue.should.be.equal(false)
  390. const newVerifier = accounts[7]
  391. const error = await mixer.updateVerifier(newVerifier).should.be.rejected
  392. error.reason.should.be.equal('Verifier updates have been disabled.')
  393. })
  394. })
  395. afterEach(async () => {
  396. await revertSnapshot(snapshotId.result)
  397. // eslint-disable-next-line require-atomic-updates
  398. snapshotId = await takeSnapshot()
  399. tree = new MerkleTree(
  400. levels,
  401. zeroValue,
  402. null,
  403. prefix,
  404. )
  405. })
  406. })