tornado.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. // A very stripped down library version of the tornado core "cli.js"
  2. //
  3. // Source: https://github.com/tornadocash/tornado-core/blob/master/src/cli.js
  4. const fs = require('fs')
  5. const buildGroth16 = require('websnark/src/groth16')
  6. const snarkjs = require('snarkjs')
  7. const crypto = require('crypto')
  8. const circomlib = require('circomlib')
  9. const bigInt = snarkjs.bigInt
  10. const merkleTree = require('fixed-merkle-tree')
  11. const websnarkUtils = require('websnark/src/utils')
  12. let circuit, proving_key, groth16, netId, MERKLE_TREE_HEIGHT
  13. const PRIME_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
  14. /** Generate random number of specified byte length */
  15. const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes));
  16. /** Compute pedersen hash */
  17. const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0];
  18. /** BigNumber to hex string of specified length */
  19. export function toHex(number, length = 32) {
  20. const str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)
  21. return '0x' + str.padStart(length * 2, '0')
  22. }
  23. // Wasm is little endian
  24. function proofToLE(proof) {
  25. const segments = proof.slice(2).match(/.{1,64}/g);
  26. const swapped = segments.map(s => swapEndianness(s))
  27. return '0x' + swapped.join('')
  28. }
  29. function swapEndianness(hexString) {
  30. //const hexString = bigInt.toString(16); // Convert to hexadecimal string
  31. const paddedHexString = hexString.length % 2 !== 0 ? '0' + hexString : hexString; // Pad with zeros if needed
  32. const byteSegments = paddedHexString.match(/.{1,2}/g); // Split into two-character segments
  33. const reversedSegments = byteSegments.reverse(); // Reverse the order of segments
  34. const reversedHexString = reversedSegments.join(''); // Join segments back into a string
  35. return reversedHexString; // Parse the reversed string as a hexadecimal value
  36. }
  37. function parseNote(noteString) {
  38. const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
  39. const match = noteRegex.exec(noteString)
  40. if (!match) {
  41. throw new Error('The note has invalid format')
  42. }
  43. const buf = Buffer.from(match.groups.note, 'hex')
  44. const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
  45. const secret = bigInt.leBuff2int(buf.slice(31, 62))
  46. const deposit = createDeposit({ nullifier, secret })
  47. const netId = Number(match.groups.netId)
  48. return { currency: match.groups.currency, amount: match.groups.amount, netId, deposit }
  49. }
  50. export async function init_snark({ networkId = 43, merkle_tree_height = 20 }) {
  51. netId = networkId;
  52. MERKLE_TREE_HEIGHT = merkle_tree_height;
  53. circuit = require(__dirname + '/tornado-cli/build/circuits/withdraw.json');
  54. proving_key = fs.readFileSync(__dirname + '/tornado-cli/build/circuits/withdraw_proving_key.bin').buffer;
  55. groth16 = await buildGroth16();
  56. }
  57. function createDeposit({ nullifier, secret }) {
  58. const deposit = { nullifier, secret }
  59. deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)]);
  60. deposit.commitment = pedersenHash(deposit.preimage);
  61. deposit.commitmentHex = toHex(deposit.commitment);
  62. deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31));
  63. deposit.nullifierHex = toHex(deposit.nullifierHash);
  64. return deposit;
  65. }
  66. export function createNote({ currency = 'ETH', amount = 1000000000000 }) {
  67. const deposit = createDeposit({ nullifier: rbigint(31), secret: rbigint(31) });
  68. const note = toHex(deposit.preimage, 62);
  69. const noteString = `tornado-${currency}-${amount}-${netId}-${note}`;
  70. // console.log(`Your commitment: ${toHex(deposit.commitment, 32)}`); // Uncomment for debug
  71. return { noteString, commitment: toHex(deposit.commitment) };
  72. }
  73. // The 'leaves' argument is supposed to be a list of commitments sorted by their leafIndex (chronologically sorted)
  74. async function generateMerkleProof(deposit, leafIndex, leaves) {
  75. console.log('generating merkle proof');
  76. let tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves);
  77. const { pathElements, pathIndices } = tree.path(leafIndex);
  78. return { pathElements, pathIndices, root: tree.root() }
  79. }
  80. async function generateProof({ deposit, recipient, leaves }) {
  81. const leafIndex = leaves.indexOf(toHex(deposit.commitment));
  82. const { root, pathElements, pathIndices } = await generateMerkleProof(deposit, leafIndex, leaves);
  83. const input = {
  84. // Public snark inputs
  85. root: root,
  86. nullifierHash: deposit.nullifierHash,
  87. recipient: bigInt(recipient),
  88. relayer: bigInt(0),
  89. fee: bigInt(0),
  90. refund: bigInt(0),
  91. // Private snark inputs
  92. nullifier: deposit.nullifier,
  93. secret: deposit.secret,
  94. pathElements: pathElements,
  95. pathIndices: pathIndices,
  96. }
  97. console.log('Generating SNARK proof');
  98. console.time('Proof time');
  99. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key);
  100. const { proof } = websnarkUtils.toSolidityInput(proofData);
  101. console.timeEnd('Proof time');
  102. const args = [
  103. toHex(input.root),
  104. toHex(input.nullifierHash),
  105. toHex(input.recipient),
  106. toHex(input.relayer, 20),
  107. toHex(input.fee),
  108. toHex(input.refund),
  109. ]
  110. // console.log(args); // uncomment for debug
  111. return { proof: proofToLE(proof), args }
  112. }
  113. export async function withdraw(to, noteString, leaves) {
  114. // Substrate 32 byte addrs aren't necessarily within the finite field (as opposed to ETH addresses).
  115. // This hack naturally makes it work regardless. Maybe it would even be fine in production too.
  116. const recipient = to % PRIME_FIELD;
  117. const parsed_note = parseNote(noteString);
  118. return await generateProof({ deposit: parsed_note.deposit, recipient, leaves });
  119. }