Jelajahi Sumber

Tornado cash on Substrate :rocket: (#1360)

Cyrill Leutwiler 2 tahun lalu
induk
melakukan
c98dfd3fa7

+ 2 - 0
.github/workflows/test.yml

@@ -310,6 +310,8 @@ jobs:
     steps:
     - name: Checkout sources
       uses: actions/checkout@v3
+      with:
+        submodules: recursive
       # We can't run substrate as a github actions service, since it requires
       # command line arguments. See https://github.com/actions/runner/pull/1152
     - name: Start substrate

+ 3 - 0
.gitmodules

@@ -1,3 +1,6 @@
 [submodule "solang-parser/testdata/solidity"]
 	path = testdata/solidity
 	url = https://github.com/ethereum/solidity
+[submodule "integration/substrate/tornado/tornado-cli"]
+	path = integration/substrate/tornado/tornado-cli
+	url = https://github.com/tornadocash/tornado-cli.git

+ 3 - 2
integration/substrate/build.sh

@@ -1,11 +1,12 @@
 #!/bin/bash
 set -e
 
-dup_contracts=$(grep -r '^contract .* {' | awk '{ print $2 }' | sort | uniq -d)
+dup_contracts=$(grep -r '^contract .* {' | grep -v node_modules | awk '{ print $2 }' | sort | uniq -d)
 if [[ $dup_contracts ]]; then
 	echo "Found contract with duplicate names: ${dup_contracts}"
 	/bin/false
 else
-	parallel solang compile -v -g --target substrate ::: *.sol test/*.sol ; solang compile -v --target substrate --release release_version.sol ;
+	parallel solang compile -v -g --target substrate ::: *.sol test/*.sol tornado/contracts/*.sol
+	solang compile -v --target substrate --release release_version.sol
 fi
 

+ 4 - 4
integration/substrate/index.ts

@@ -4,7 +4,7 @@ import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
 import { convertWeight } from '@polkadot/api-contract/base/util';
 import { CodePromise, ContractPromise } from '@polkadot/api-contract';
 import { SubmittableExtrinsic } from '@polkadot/api/types';
-import { Codec, ISubmittableResult } from '@polkadot/types/types';
+import { AnyNumber, Codec, ISubmittableResult } from '@polkadot/types/types';
 import { KeyringPair } from '@polkadot/keyring/types';
 import expect from 'expect';
 import { ContractExecResultResult, WeightV2 } from '@polkadot/types/interfaces';
@@ -80,19 +80,19 @@ export function transaction(tx: SubmittableExtrinsic<"promise", ISubmittableResu
 }
 
 // Returns the required gas estimated from a dry run
-export async function weight(api: ApiPromise, contract: ContractPromise, message: string, args?: unknown[], value?: number) {
+export async function weight(api: ApiPromise, contract: ContractPromise, message: string, args?: unknown[], value?: AnyNumber) {
   let res = await dry_run(api, contract, message, args, value);
   return res.gasRequired
 }
 
 // Returns the debug buffer from the dry run result
-export async function debug_buffer(api: ApiPromise, contract: ContractPromise, message: string, args?: unknown[], value?: number) {
+export async function debug_buffer(api: ApiPromise, contract: ContractPromise, message: string, args?: unknown[], value?: AnyNumber) {
   let res = await dry_run(api, contract, message, args, value);
   return res.debugMessage.toHuman()
 }
 
 // Return dry run result
-export async function dry_run(api: ApiPromise, contract: ContractPromise, message: string, args?: unknown[], value?: number) {
+export async function dry_run(api: ApiPromise, contract: ContractPromise, message: string, args?: unknown[], value?: AnyNumber) {
   const ALICE = new Keyring({ type: 'sr25519' }).addFromUri('//Alice').address;
   const msg = contract.abi.findMessage(message);
   const dry = await api.call.contractsApi.call(ALICE, contract.address, value ? value : 0, null, null, msg.toU8a(args ? args : []));

+ 6 - 2
integration/substrate/package.json

@@ -4,7 +4,7 @@
   "description": "Integration tests with Solang and Substrate",
   "main": "index.js",
   "scripts": {
-    "test": "tsc; ts-mocha -t 20000 *.spec.ts",
+    "test": "tsc; ts-mocha -t 20000 --exit *.spec.ts",
     "build": "./build.sh",
     "build-ink": "docker run --rm -v $(pwd)/ink/caller:/opt/contract ghcr.io/hyperledger/solang-substrate-ci:e41a9c0 cargo contract build --release --manifest-path /opt/contract/Cargo.toml"
   },
@@ -34,6 +34,10 @@
     "@polkadot/api-contract": "^10.6",
     "@polkadot/keyring": "^12.1",
     "@polkadot/types": "^10.6",
-    "@polkadot/util-crypto": "^12.1"
+    "@polkadot/util-crypto": "^12.1",
+    "websnark": "git+https://github.com/tornadocash/websnark.git#4c0af6a8b65aabea3c09f377f63c44e7a58afa6d",
+    "snarkjs": "git+https://github.com/tornadocash/snarkjs.git#869181cfaf7526fe8972073d31655493a04326d5",
+    "circomlib": "git+https://github.com/tornadocash/circomlib.git#c372f14d324d57339c88451834bf2824e73bbdbc",
+    "fixed-merkle-tree": "^0.6.0"
   }
 }

+ 128 - 0
integration/substrate/tornado.spec.ts

@@ -0,0 +1,128 @@
+// Tests against the tornado cash core contracts.
+// The tornado contracts used here contain minor mechanical changes to work fine on Substrate.
+// The ZK-SNARK setup is the same as ETH Tornado on mainnet.
+// On the node, the MiMC sponge hash (available as EVM bytecode) and bn128 curve operations
+// (precompiled contracts on Ethereum) are expected to be implemented as chain extensions.
+
+import expect from 'expect';
+import { weight, createConnection, deploy, transaction, aliceKeypair, daveKeypair, debug_buffer, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { createNote, init_snark, toHex, withdraw, } from './tornado/tornado'
+
+type Deposit = { noteString: string; commitment: string; };
+
+let deposits: Deposit[];
+const denomination = 1000000000000n;
+const merkle_tree_height = 20;
+
+function addressToBigInt(uint8Array: Uint8Array): bigint {
+    let result = BigInt(0);
+    for (let i = 0; i < uint8Array.length; i++) {
+        result <<= BigInt(8); // Left shift by 8 bits
+        result += BigInt(uint8Array[i]); // Add the current byte
+    }
+    return result;
+}
+
+// Generate a ZK proof needed to withdraw funds. Uses the deposit at the given `index`. 
+async function generateProof(recipient: KeyringPair, index: number): Promise<string[]> {
+    const to = addressToBigInt(recipient.addressRaw);
+    // In production, we'd fetch and parse all events, which is too cumbersome for this PoC.
+    const leaves = deposits.map(e => e.commitment);
+    const proof = await withdraw(to, deposits[index].noteString, leaves);
+    return [
+        proof.proof,
+        proof.args[0],  // Merkle root
+        proof.args[1],  // Nullifier hash
+        toHex(to),      // The contract will mod it over the finite field
+    ];
+}
+
+describe('Deploy the tornado contract, create 2 deposits and withdraw them afterwards', () => {
+    let conn: ApiPromise;
+    let tornado: ContractPromise;
+    let alice: KeyringPair;
+    let dave: KeyringPair;
+
+    before(async function () {
+        alice = aliceKeypair();
+        dave = daveKeypair();
+
+        // Deploy hasher, verifier and tornado contracts
+        conn = await createConnection();
+        const hasher_contract = await deploy(conn, alice, 'Hasher.contract', 0n);
+        const verifier_contract = await deploy(conn, alice, 'Verifier.contract', 0n);
+        const parameters =
+            [
+                verifier_contract.address,
+                hasher_contract.address,
+                denomination,
+                merkle_tree_height
+            ];
+        const tornado_contract = await deploy(conn, alice, 'NativeTornado.contract', 0n, ...parameters);
+        tornado = new ContractPromise(conn, tornado_contract.abi, tornado_contract.address);
+
+        // Deposit some funds to the tornado contract
+        await init_snark({});
+        deposits = [createNote({}), createNote({})];
+
+        let gasLimit = await weight(conn, tornado, 'deposit', [deposits[0].commitment], denomination);
+        let tx = tornado.tx.deposit({ gasLimit, value: denomination }, deposits[0].commitment);
+        await transaction(tx, alice);
+
+        gasLimit = await weight(conn, tornado, 'deposit', [deposits[1].commitment], denomination);
+        tx = tornado.tx.deposit({ gasLimit, value: denomination }, deposits[1].commitment);
+        await transaction(tx, dave);
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('Withdraws funds deposited by alice to dave', async function () {
+        const { data: { free: balanceBefore } } = await conn.query.system.account(dave.address);
+
+        const parameters = await generateProof(dave, 0);
+        const gasLimit = await weight(conn, tornado, 'withdraw', parameters);
+        await transaction(tornado.tx.withdraw({ gasLimit }, ...parameters), alice);
+
+        expect(balanceBefore.toBigInt() + denomination)
+            .toEqual((await conn.query.system.account(dave.address)).data.free.toBigInt());
+
+        expect(await debug_buffer(conn, tornado, 'withdraw', parameters))
+            .toContain('The note has been already spent');
+    });
+
+    it('Withdraws funds deposited by dave to alice', async function () {
+        const { data: { free: balanceBefore } } = await conn.query.system.account(dave.address);
+
+        const parameters = await generateProof(dave, 1);
+        const gasLimit = await weight(conn, tornado, 'withdraw', parameters);
+        await transaction(tornado.tx.withdraw({ gasLimit }, ...parameters), alice);
+
+        expect(balanceBefore.toBigInt() + denomination)
+            .toEqual((await conn.query.system.account(dave.address)).data.free.toBigInt());
+
+        expect(await debug_buffer(conn, tornado, 'withdraw', parameters))
+            .toContain('The note has been already spent');
+    });
+
+    it('Fails to withdraw without a valid proof', async function () {
+        // Without a corresponding deposit, this merkle root should not exist yet
+        deposits.push(createNote({}));
+        let parameters = await generateProof(alice, 2);
+        expect(await debug_buffer(conn, tornado, 'withdraw', parameters))
+            .toContain('Cannot find your merkle root');
+
+        const gasLimit = await weight(conn, tornado, 'deposit', [deposits[2].commitment], denomination);
+        const tx = tornado.tx.deposit({ gasLimit, value: denomination }, deposits[2].commitment);
+        await transaction(tx, alice);
+
+        // Messing up the proof should result in a curve pairing failure
+        parameters[0] = parameters[0].substring(0, parameters[0].length - 4) + "0000";
+        expect(await debug_buffer(conn, tornado, 'withdraw', parameters))
+            .toContain('pairing-opcode-failed');
+    });
+});

+ 9 - 0
integration/substrate/tornado/contracts/Hasher.sol

@@ -0,0 +1,9 @@
+import "substrate";
+
+contract Hasher {
+    function MiMCSponge(uint256 xL, uint256 xR) external returns (uint256 outL, uint256 outR) {
+        (uint32 ret, bytes output) = chain_extension(220, abi.encode(xL, xR));
+        assert(ret == 0);
+        (outL, outR) = abi.decode(output, (uint256, uint256));
+    }
+}

+ 164 - 0
integration/substrate/tornado/contracts/MerkleTreeWithHistory.sol

@@ -0,0 +1,164 @@
+// https://tornado.cash
+/*
+ * d888888P                                           dP              a88888b.                   dP
+ *    88                                              88             d8'   `88                   88
+ *    88    .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b.    88        .d8888b. .d8888b. 88d888b.
+ *    88    88'  `88 88'  `88 88'  `88 88'  `88 88'  `88 88'  `88    88        88'  `88 Y8ooooo. 88'  `88
+ *    88    88.  .88 88       88    88 88.  .88 88.  .88 88.  .88 dP Y8.   .88 88.  .88       88 88    88
+ *    dP    `88888P' dP       dP    dP `88888P8 `88888P8 `88888P' 88  Y88888P' `88888P8 `88888P' dP    dP
+ * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
+ */
+
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.7.0;
+
+interface IHasher {
+    function MiMCSponge(uint256 in_xL, uint256 in_xR) external pure returns (uint256 xL, uint256 xR);
+}
+
+contract MerkleTreeWithHistory {
+    uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
+    uint256 public constant ZERO_VALUE = 21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE
+    IHasher public immutable hasher;
+
+    uint32 public levels;
+
+    // The following variables are made public for easier testing and debugging.
+    // They are not supposed to be accessed in regular code
+
+    // filledSubtrees and roots could be bytes32[size], but using mappings is cheaper because
+    // they do not have index range check on every interaction
+    mapping(uint256 => bytes32) public filledSubtrees;
+    mapping(uint256 => bytes32) public roots;
+    uint32 public constant ROOT_HISTORY_SIZE = 30;
+    uint32 public currentRootIndex = 0;
+    uint32 public nextIndex = 0;
+
+    constructor(uint32 _levels, IHasher _hasher) {
+        require(_levels > 0, "_levels should be greater than zero");
+        require(_levels < 32, "_levels should be less than 32");
+        levels = _levels;
+        hasher = _hasher;
+
+        for (uint32 i = 0; i < _levels; i++) {
+            filledSubtrees[i] = zeros(i);
+        }
+
+        roots[0] = zeros(_levels - 1);
+    }
+
+    /**
+    @dev Hash 2 tree leaves, returns MiMC(_left, _right)
+  */
+    function hashLeftRight(
+        IHasher _hasher,
+        bytes32 _left,
+        bytes32 _right
+    ) public pure returns (bytes32) {
+        require(uint256(_left) < FIELD_SIZE, "_left should be inside the field");
+        require(uint256(_right) < FIELD_SIZE, "_right should be inside the field");
+        uint256 R = uint256(_left);
+        uint256 C = 0;
+        (R, C) = _hasher.MiMCSponge(R, C);
+        R = addmod(R, uint256(_right), FIELD_SIZE);
+        (R, C) = _hasher.MiMCSponge(R, C);
+        return bytes32(R);
+    }
+
+    function _insert(bytes32 _leaf) internal returns (uint32 index) {
+        uint32 _nextIndex = nextIndex;
+        require(_nextIndex != uint32(2)**levels, "Merkle tree is full. No more leaves can be added");
+        uint32 currentIndex = _nextIndex;
+        bytes32 currentLevelHash = _leaf;
+        bytes32 left;
+        bytes32 right;
+
+        for (uint32 i = 0; i < levels; i++) {
+            if (currentIndex % 2 == 0) {
+                left = currentLevelHash;
+                right = zeros(i);
+                filledSubtrees[i] = currentLevelHash;
+            } else {
+                left = filledSubtrees[i];
+                right = currentLevelHash;
+            }
+            currentLevelHash = hashLeftRight(hasher, left, right);
+            currentIndex /= 2;
+        }
+
+        uint32 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
+        currentRootIndex = newRootIndex;
+        roots[newRootIndex] = currentLevelHash;
+        nextIndex = _nextIndex + 1;
+        return _nextIndex;
+    }
+
+    function isKnownRoot2(bytes _root) public view returns (bool) {
+        return isKnownRoot(abi.decode(_root, (bytes32)));
+    }
+
+    /**
+    @dev Whether the root is present in the root history
+  */
+    function isKnownRoot(bytes32 _root) public view returns (bool) {
+        if (_root == 0) {
+            return false;
+        }
+        uint32 _currentRootIndex = currentRootIndex;
+        uint32 i = _currentRootIndex;
+        do {
+            if (_root == roots[i]) {
+                return true;
+            }
+            if (i == 0) {
+                i = ROOT_HISTORY_SIZE;
+            }
+            i--;
+        } while (i != _currentRootIndex);
+        return false;
+    }
+
+    /**
+    @dev Returns the last root
+  */
+    function getLastRoot() public view returns (bytes32) {
+        return roots[currentRootIndex];
+    }
+
+    /// @dev provides Zero (Empty) elements for a MiMC MerkleTree. Up to 32 levels
+    function zeros(uint256 i) public pure returns (bytes32) {
+        if (i == 0) return bytes32(0x2fe54c60d3acabf3343a35b6eba15db4821b340f76e741e2249685ed4899af6c);
+        else if (i == 1) return bytes32(0x256a6135777eee2fd26f54b8b7037a25439d5235caee224154186d2b8a52e31d);
+        else if (i == 2) return bytes32(0x1151949895e82ab19924de92c40a3d6f7bcb60d92b00504b8199613683f0c200);
+        else if (i == 3) return bytes32(0x20121ee811489ff8d61f09fb89e313f14959a0f28bb428a20dba6b0b068b3bdb);
+        else if (i == 4) return bytes32(0x0a89ca6ffa14cc462cfedb842c30ed221a50a3d6bf022a6a57dc82ab24c157c9);
+        else if (i == 5) return bytes32(0x24ca05c2b5cd42e890d6be94c68d0689f4f21c9cec9c0f13fe41d566dfb54959);
+        else if (i == 6) return bytes32(0x1ccb97c932565a92c60156bdba2d08f3bf1377464e025cee765679e604a7315c);
+        else if (i == 7) return bytes32(0x19156fbd7d1a8bf5cba8909367de1b624534ebab4f0f79e003bccdd1b182bdb4);
+        else if (i == 8) return bytes32(0x261af8c1f0912e465744641409f622d466c3920ac6e5ff37e36604cb11dfff80);
+        else if (i == 9) return bytes32(0x0058459724ff6ca5a1652fcbc3e82b93895cf08e975b19beab3f54c217d1c007);
+        else if (i == 10) return bytes32(0x1f04ef20dee48d39984d8eabe768a70eafa6310ad20849d4573c3c40c2ad1e30);
+        else if (i == 11) return bytes32(0x1bea3dec5dab51567ce7e200a30f7ba6d4276aeaa53e2686f962a46c66d511e5);
+        else if (i == 12) return bytes32(0x0ee0f941e2da4b9e31c3ca97a40d8fa9ce68d97c084177071b3cb46cd3372f0f);
+        else if (i == 13) return bytes32(0x1ca9503e8935884501bbaf20be14eb4c46b89772c97b96e3b2ebf3a36a948bbd);
+        else if (i == 14) return bytes32(0x133a80e30697cd55d8f7d4b0965b7be24057ba5dc3da898ee2187232446cb108);
+        else if (i == 15) return bytes32(0x13e6d8fc88839ed76e182c2a779af5b2c0da9dd18c90427a644f7e148a6253b6);
+        else if (i == 16) return bytes32(0x1eb16b057a477f4bc8f572ea6bee39561098f78f15bfb3699dcbb7bd8db61854);
+        else if (i == 17) return bytes32(0x0da2cb16a1ceaabf1c16b838f7a9e3f2a3a3088d9e0a6debaa748114620696ea);
+        else if (i == 18) return bytes32(0x24a3b3d822420b14b5d8cb6c28a574f01e98ea9e940551d2ebd75cee12649f9d);
+        else if (i == 19) return bytes32(0x198622acbd783d1b0d9064105b1fc8e4d8889de95c4c519b3f635809fe6afc05);
+        else if (i == 20) return bytes32(0x29d7ed391256ccc3ea596c86e933b89ff339d25ea8ddced975ae2fe30b5296d4);
+        else if (i == 21) return bytes32(0x19be59f2f0413ce78c0c3703a3a5451b1d7f39629fa33abd11548a76065b2967);
+        else if (i == 22) return bytes32(0x1ff3f61797e538b70e619310d33f2a063e7eb59104e112e95738da1254dc3453);
+        else if (i == 23) return bytes32(0x10c16ae9959cf8358980d9dd9616e48228737310a10e2b6b731c1a548f036c48);
+        else if (i == 24) return bytes32(0x0ba433a63174a90ac20992e75e3095496812b652685b5e1a2eae0b1bf4e8fcd1);
+        else if (i == 25) return bytes32(0x019ddb9df2bc98d987d0dfeca9d2b643deafab8f7036562e627c3667266a044c);
+        else if (i == 26) return bytes32(0x2d3c88b23175c5a5565db928414c66d1912b11acf974b2e644caaac04739ce99);
+        else if (i == 27) return bytes32(0x2eab55f6ae4e66e32c5189eed5c470840863445760f5ed7e7b69b2a62600f354);
+        else if (i == 28) return bytes32(0x002df37a2642621802383cf952bf4dd1f32e05433beeb1fd41031fb7eace979d);
+        else if (i == 29) return bytes32(0x104aeb41435db66c3e62feccc1d6f5d98d0a0ed75d1374db457cf462e3a1f427);
+        else if (i == 30) return bytes32(0x1f3c6fd858e9a7d4b0d1f38e256a09d81d5a5e3c963987e2d4b814cfab7c6ebb);
+        else if (i == 31) return bytes32(0x2c7a07d20dff79d01fecedc1134284a8d08436606c93693b67e333f671bf69cc);
+        else revert("Index out of bounds");
+    }
+}

+ 30 - 0
integration/substrate/tornado/contracts/NativeTornado.sol

@@ -0,0 +1,30 @@
+import "./Tornado.sol";
+
+contract NativeTornado is Tornado {
+    constructor(
+        IVerifier _verifier,
+        IHasher _hasher,
+        uint128 _denomination,
+        uint32 _merkleTreeHeight
+    ) Tornado(_verifier, _hasher, _denomination, _merkleTreeHeight) {}
+
+    function _processDeposit() internal view override {
+        require(
+            msg.value == denomination,
+            "Send exactly {} along with transaction".format(denomination)
+        );
+    }
+
+    function _processWithdraw(
+        address payable _recipient,
+        address payable _relayer,
+        uint128 _fee,
+        uint128 _refund
+    ) internal override {
+        require(msg.value == 0, "Message value is supposed to be zero");
+        require(_refund == 0, "Refund value is supposed to be zero");
+        require(_fee == 0, "Relayers are not used with this PoC");
+        require(uint256(_relayer) == 0, "Relayers are not used with this PoC");
+        _recipient.transfer(denomination);
+    }
+}

+ 132 - 0
integration/substrate/tornado/contracts/Tornado.sol

@@ -0,0 +1,132 @@
+// https://tornado.cash
+/*
+ * d888888P                                           dP              a88888b.                   dP
+ *    88                                              88             d8'   `88                   88
+ *    88    .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b.    88        .d8888b. .d8888b. 88d888b.
+ *    88    88'  `88 88'  `88 88'  `88 88'  `88 88'  `88 88'  `88    88        88'  `88 Y8ooooo. 88'  `88
+ *    88    88.  .88 88       88    88 88.  .88 88.  .88 88.  .88 dP Y8.   .88 88.  .88       88 88    88
+ *    dP    `88888P' dP       dP    dP `88888P8 `88888P8 `88888P' 88  Y88888P' `88888P8 `88888P' dP    dP
+ * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
+ */
+
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.7.0;
+
+import "./MerkleTreeWithHistory.sol";
+
+// Reentrancy is disabled by default anyways
+//import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
+
+interface IVerifier {
+    function verifyProof(bytes memory _proof, uint256[6] memory _input) external returns (bool);
+}
+
+abstract contract Tornado is MerkleTreeWithHistory {
+    IVerifier public immutable verifier;
+    uint128 public denomination;
+
+    mapping(bytes32 => bool) public nullifierHashes;
+    // we store all commitments just to prevent accidental deposits with the same commitment
+    mapping(bytes32 => bool) public commitments;
+
+    event Deposit(bytes32 indexed commitment, uint32 leafIndex, uint256 timestamp);
+    event Withdrawal(address to, bytes32 nullifierHash, address indexed relayer, uint256 fee);
+
+    /**
+    @dev The constructor
+    @param _verifier the address of SNARK verifier for this contract
+    @param _hasher the address of MiMC hash contract
+    @param _denomination transfer amount for each deposit
+    @param _merkleTreeHeight the height of deposits' Merkle Tree
+  */
+    constructor(
+        IVerifier _verifier,
+        IHasher _hasher,
+        uint128 _denomination,
+        uint32 _merkleTreeHeight
+    ) MerkleTreeWithHistory(_merkleTreeHeight, _hasher) {
+        require(_denomination > 0, "denomination should be greater than 0");
+        verifier = _verifier;
+        denomination = _denomination;
+    }
+
+    /**
+    @dev Deposit funds into the contract. The caller must send (for ETH) or approve (for ERC20) value equal to or `denomination` of this instance.
+    @param _commit the note commitment, which is PedersenHash(nullifier + secret)
+  */
+    function deposit(bytes _commit) external payable {
+        bytes32 _commitment = abi.decode(_commit, (bytes32));
+        require(!commitments[_commitment], "The commitment has been submitted");
+
+        uint32 insertedIndex = _insert(_commitment);
+        commitments[_commitment] = true;
+        _processDeposit();
+
+        emit Deposit(_commitment, insertedIndex, block.timestamp);
+    }
+
+    /** @dev this function is defined in a child contract */
+    function _processDeposit() internal virtual;
+
+    /**
+    @dev Withdraw a deposit from the contract. `proof` is a zkSNARK proof data, and input is an array of circuit public inputs
+    `input` array consists of:
+      - merkle root of all deposits in the contract
+      - hash of unique deposit nullifier to prevent double spends
+      - the recipient of funds
+      - optional fee that goes to the transaction sender (usually a relay)
+  */
+    function withdraw(
+        bytes calldata _proof,
+        bytes _root_bytes,
+        bytes _nullifierHash_bytes,
+        address payable _recipient
+    ) external payable {
+        // We don't need these for our PoC
+        address payable _relayer = address payable(0);
+        uint256 _fee = 0;
+        uint256 _refund = 0;
+        // Substrate 32 byte addresses are not necessarily within the field the SNARK uses.
+        uint256 recipient_input = uint256(_recipient) % 21888242871839275222246405745257275088548364400416034343698204186575808495617;
+
+        bytes32 _root = abi.decode(_root_bytes, (bytes32));
+        bytes32 _nullifierHash = abi.decode(_nullifierHash_bytes, (bytes32));
+        // require(_fee <= denomination, "Fee exceeds transfer value");
+        require(!nullifierHashes[_nullifierHash], "The note has been already spent");
+        require(isKnownRoot(_root), "Cannot find your merkle root"); // Make sure to use a recent one
+        require(
+            verifier.verifyProof(
+                _proof,
+                [uint256(_root), uint256(_nullifierHash), recipient_input, uint256(_relayer), _fee, _refund]
+            ),
+            "Invalid withdraw proof"
+        );
+
+        nullifierHashes[_nullifierHash] = true;
+        _processWithdraw(_recipient, _relayer, 0, 0);
+        emit Withdrawal(_recipient, _nullifierHash, _relayer, 0);
+    }
+
+    /** @dev this function is defined in a child contract */
+    function _processWithdraw(
+        address payable _recipient,
+        address payable _relayer,
+        uint128 _fee,
+        uint128 _refund
+    ) internal virtual;
+
+    /** @dev whether a note is already spent */
+    function isSpent(bytes32 _nullifierHash) public view returns (bool) {
+        return nullifierHashes[_nullifierHash];
+    }
+
+    /** @dev whether an array of notes is already spent */
+    function isSpentArray(bytes32[] calldata _nullifierHashes) external view returns (bool[] memory spent) {
+        spent = new bool[](_nullifierHashes.length);
+        for (uint256 i = 0; i < _nullifierHashes.length; i++) {
+            if (isSpent(_nullifierHashes[i])) {
+                spent[i] = true;
+            }
+        }
+    }
+}

+ 212 - 0
integration/substrate/tornado/contracts/Verifier.sol

@@ -0,0 +1,212 @@
+/**
+ *Submitted for verification at Etherscan.io on 2020-05-12
+*/
+
+// https://tornado.cash Verifier.sol generated by trusted setup ceremony.
+/*
+* d888888P                                           dP              a88888b.                   dP
+*    88                                              88             d8'   `88                   88
+*    88    .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b.    88        .d8888b. .d8888b. 88d888b.
+*    88    88'  `88 88'  `88 88'  `88 88'  `88 88'  `88 88'  `88    88        88'  `88 Y8ooooo. 88'  `88
+*    88    88.  .88 88       88    88 88.  .88 88.  .88 88.  .88 dP Y8.   .88 88.  .88       88 88    88
+*    dP    `88888P' dP       dP    dP `88888P8 `88888P8 `88888P' 88  Y88888P' `88888P8 `88888P' dP    dP
+* ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
+*/
+// SPDX-License-Identifier: MIT
+// Copyright 2017 Christian Reitwiessner
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// 2019 OKIMS
+
+pragma solidity ^0.7.0;
+
+import "substrate";
+
+library Pairing {
+  uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
+
+  struct G1Point {
+    uint256 X;
+    uint256 Y;
+  }
+
+  // Encoding of field elements is: X[0] * z + X[1]
+  struct G2Point {
+    uint256[2] X;
+    uint256[2] Y;
+  }
+
+  /*
+   * @return The negation of p, i.e. p.plus(p.negate()) should be zero.
+   */
+  function negate(G1Point memory p) internal pure returns (G1Point memory) {
+    // The prime q in the base field F_q for G1
+    if (p.X == 0 && p.Y == 0) {
+      return G1Point(0, 0);
+    } else {
+      return G1Point(p.X, PRIME_Q - (p.Y % PRIME_Q));
+    }
+  }
+
+  /*
+   * @return r the sum of two points of G1
+   */
+  function plus(
+    G1Point memory p1,
+    G1Point memory p2
+  ) internal returns (G1Point memory r) {
+    uint256[4] memory input;
+    input[0] = p1.X;
+    input[1] = p1.Y;
+    input[2] = p2.X;
+    input[3] = p2.Y;
+
+    (uint32 success, bytes output) = chain_extension(6, abi.encode(input));
+    require(success == 0, "pairing-add-failed");
+    r = abi.decode(output, (G1Point));
+  }
+
+  /*
+   * @return r the product of a point on G1 and a scalar, i.e.
+   *         p == p.scalar_mul(1) and p.plus(p) == p.scalar_mul(2) for all
+   *         points p.
+   */
+  function scalar_mul(G1Point memory p, uint256 s) internal returns (G1Point memory r) {
+    uint256[3] memory input;
+    input[0] = p.X;
+    input[1] = p.Y;
+    input[2] = s;
+
+    (uint32 success, bytes output) = chain_extension(7, abi.encode(input));
+    require(success == 0, "pairing-mul-failed");
+    r = abi.decode(output, (G1Point));
+  }
+
+  /* @return The result of computing the pairing check
+   *         e(p1[0], p2[0]) *  .... * e(p1[n], p2[n]) == 1
+   *         For example,
+   *         pairing([P1(), P1().negate()], [P2(), P2()]) should return true.
+   */
+  function pairing(
+    G1Point memory a1,
+    G2Point memory a2,
+    G1Point memory b1,
+    G2Point memory b2,
+    G1Point memory c1,
+    G2Point memory c2,
+    G1Point memory d1,
+    G2Point memory d2
+  ) internal returns (bool) {
+    G1Point[4] memory p1 = [a1, b1, c1, d1];
+    G2Point[4] memory p2 = [a2, b2, c2, d2];
+
+    uint256[24] memory input;
+
+    for (uint256 i = 0; i < 4; i++) {
+      uint256 j = i * 6;
+      input[j + 0] = p1[i].X;
+      input[j + 1] = p1[i].Y;
+      input[j + 2] = p2[i].X[0];
+      input[j + 3] = p2[i].X[1];
+      input[j + 4] = p2[i].Y[0];
+      input[j + 5] = p2[i].Y[1];
+    }
+
+    (uint32 success, bytes out) = chain_extension(8, abi.encode(input));
+    require(success == 0, "pairing-opcode-failed");
+    return out[0] != 0;
+  }
+}
+
+contract Verifier {
+  uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
+  uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583;
+  using Pairing for *;
+
+  struct VerifyingKey {
+    Pairing.G1Point alfa1;
+    Pairing.G2Point beta2;
+    Pairing.G2Point gamma2;
+    Pairing.G2Point delta2;
+    Pairing.G1Point[7] IC;
+  }
+
+  struct Proof {
+    Pairing.G1Point A;
+    Pairing.G2Point B;
+    Pairing.G1Point C;
+  }
+
+function verifyingKey() internal pure returns (VerifyingKey memory vk) {
+        vk.alfa1 = Pairing.G1Point(uint256(20692898189092739278193869274495556617788530808486270118371701516666252877969), uint256(11713062878292653967971378194351968039596396853904572879488166084231740557279));
+        vk.beta2 = Pairing.G2Point([uint256(12168528810181263706895252315640534818222943348193302139358377162645029937006), uint256(281120578337195720357474965979947690431622127986816839208576358024608803542)], [uint256(16129176515713072042442734839012966563817890688785805090011011570989315559913), uint256(9011703453772030375124466642203641636825223906145908770308724549646909480510)]);
+        vk.gamma2 = Pairing.G2Point([uint256(11559732032986387107991004021392285783925812861821192530917403151452391805634), uint256(10857046999023057135944570762232829481370756359578518086990519993285655852781)], [uint256(4082367875863433681332203403145435568316851327593401208105741076214120093531), uint256(8495653923123431417604973247489272438418190587263600148770280649306958101930)]);
+        vk.delta2 = Pairing.G2Point([uint256(21280594949518992153305586783242820682644996932183186320680800072133486887432), uint256(150879136433974552800030963899771162647715069685890547489132178314736470662)], [uint256(1081836006956609894549771334721413187913047383331561601606260283167615953295), uint256(11434086686358152335540554643130007307617078324975981257823476472104616196090)]);
+        vk.IC[0] = Pairing.G1Point(uint256(16225148364316337376768119297456868908427925829817748684139175309620217098814), uint256(5167268689450204162046084442581051565997733233062478317813755636162413164690));
+        vk.IC[1] = Pairing.G1Point(uint256(12882377842072682264979317445365303375159828272423495088911985689463022094260), uint256(19488215856665173565526758360510125932214252767275816329232454875804474844786));
+        vk.IC[2] = Pairing.G1Point(uint256(13083492661683431044045992285476184182144099829507350352128615182516530014777), uint256(602051281796153692392523702676782023472744522032670801091617246498551238913));
+        vk.IC[3] = Pairing.G1Point(uint256(9732465972180335629969421513785602934706096902316483580882842789662669212890), uint256(2776526698606888434074200384264824461688198384989521091253289776235602495678));
+        vk.IC[4] = Pairing.G1Point(uint256(8586364274534577154894611080234048648883781955345622578531233113180532234842), uint256(21276134929883121123323359450658320820075698490666870487450985603988214349407));
+        vk.IC[5] = Pairing.G1Point(uint256(4910628533171597675018724709631788948355422829499855033965018665300386637884), uint256(20532468890024084510431799098097081600480376127870299142189696620752500664302));
+        vk.IC[6] = Pairing.G1Point(uint256(15335858102289947642505450692012116222827233918185150176888641903531542034017), uint256(5311597067667671581646709998171703828965875677637292315055030353779531404812));
+
+    }
+
+  /*
+   * @returns Whether the proof is valid given the hardcoded verifying key
+   *          above and the public inputs
+   */
+  function verifyProof(
+    bytes memory proof,
+    uint256[6] memory input
+  ) public returns (bool) {
+    uint256[8] memory p = abi.decode(proof, (uint256[8]));
+
+    // Make sure that each element in the proof is less than the prime q
+    for (uint8 i = 0; i < p.length; i++) {
+      require(p[i] < PRIME_Q, "verifier-proof-element-gte-prime-q");
+    }
+
+    Proof memory _proof;
+    _proof.A = Pairing.G1Point(p[0], p[1]);
+    _proof.B = Pairing.G2Point([p[2], p[3]], [p[4], p[5]]);
+    _proof.C = Pairing.G1Point(p[6], p[7]);
+
+    VerifyingKey memory vk = verifyingKey();
+
+    // Compute the linear combination vk_x
+    Pairing.G1Point memory vk_x = Pairing.G1Point(0, 0);
+    vk_x = Pairing.plus(vk_x, vk.IC[0]);
+
+    // Make sure that every input is less than the snark scalar field
+    for (uint256 i = 0; i < input.length; i++) {
+      require(input[i] < SNARK_SCALAR_FIELD, "verifier-gte-snark-scalar-field");
+      vk_x = Pairing.plus(vk_x, Pairing.scalar_mul(vk.IC[i + 1], input[i]));
+    }
+
+    return Pairing.pairing(
+      Pairing.negate(_proof.A),
+      _proof.B,
+      vk.alfa1,
+      vk.beta2,
+      vk_x,
+      vk.gamma2,
+      _proof.C,
+      vk.delta2
+    );
+  }
+}

+ 1 - 0
integration/substrate/tornado/tornado-cli

@@ -0,0 +1 @@
+Subproject commit 378ddf8b8b92a4924037d7b64a94dbfd5a7dd6e8

+ 141 - 0
integration/substrate/tornado/tornado.js

@@ -0,0 +1,141 @@
+// A very stripped down library version of the tornado core "cli.js"
+//
+// Source: https://github.com/tornadocash/tornado-core/blob/master/src/cli.js
+
+const fs = require('fs')
+const buildGroth16 = require('websnark/src/groth16')
+const snarkjs = require('snarkjs')
+const crypto = require('crypto')
+const circomlib = require('circomlib')
+const bigInt = snarkjs.bigInt
+const merkleTree = require('fixed-merkle-tree')
+const websnarkUtils = require('websnark/src/utils')
+
+let circuit, proving_key, groth16, netId, MERKLE_TREE_HEIGHT
+
+const PRIME_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
+
+/** Generate random number of specified byte length */
+const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes));
+
+/** Compute pedersen hash */
+const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0];
+
+/** BigNumber to hex string of specified length */
+export function toHex(number, length = 32) {
+    const str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16)
+    return '0x' + str.padStart(length * 2, '0')
+}
+
+// Wasm is little endian
+function proofToLE(proof) {
+    const segments = proof.slice(2).match(/.{1,64}/g);
+    const swapped = segments.map(s => swapEndianness(s))
+    return '0x' + swapped.join('')
+}
+
+function swapEndianness(hexString) {
+    //const hexString = bigInt.toString(16); // Convert to hexadecimal string
+    const paddedHexString = hexString.length % 2 !== 0 ? '0' + hexString : hexString; // Pad with zeros if needed
+    const byteSegments = paddedHexString.match(/.{1,2}/g); // Split into two-character segments
+    const reversedSegments = byteSegments.reverse(); // Reverse the order of segments
+    const reversedHexString = reversedSegments.join(''); // Join segments back into a string
+    return reversedHexString; // Parse the reversed string as a hexadecimal value
+}
+
+function parseNote(noteString) {
+    const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
+    const match = noteRegex.exec(noteString)
+    if (!match) {
+        throw new Error('The note has invalid format')
+    }
+
+    const buf = Buffer.from(match.groups.note, 'hex')
+    const nullifier = bigInt.leBuff2int(buf.slice(0, 31))
+    const secret = bigInt.leBuff2int(buf.slice(31, 62))
+    const deposit = createDeposit({ nullifier, secret })
+    const netId = Number(match.groups.netId)
+
+    return { currency: match.groups.currency, amount: match.groups.amount, netId, deposit }
+}
+
+export async function init_snark({ networkId = 43, merkle_tree_height = 20 }) {
+    netId = networkId;
+    MERKLE_TREE_HEIGHT = merkle_tree_height;
+    circuit = require(__dirname + '/tornado-cli/build/circuits/withdraw.json');
+    proving_key = fs.readFileSync(__dirname + '/tornado-cli/build/circuits/withdraw_proving_key.bin').buffer;
+    groth16 = await buildGroth16();
+}
+
+function createDeposit({ nullifier, secret }) {
+    const deposit = { nullifier, secret }
+    deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)]);
+    deposit.commitment = pedersenHash(deposit.preimage);
+    deposit.commitmentHex = toHex(deposit.commitment);
+    deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31));
+    deposit.nullifierHex = toHex(deposit.nullifierHash);
+    return deposit;
+}
+
+export function createNote({ currency = 'ETH', amount = 1000000000000 }) {
+    const deposit = createDeposit({ nullifier: rbigint(31), secret: rbigint(31) });
+    const note = toHex(deposit.preimage, 62);
+    const noteString = `tornado-${currency}-${amount}-${netId}-${note}`;
+    // console.log(`Your commitment: ${toHex(deposit.commitment, 32)}`); // Uncomment for debug
+    return { noteString, commitment: toHex(deposit.commitment) };
+}
+
+// The 'leaves' argument is supposed to be a list of commitments sorted by their leafIndex (chronologically sorted)
+async function generateMerkleProof(deposit, leafIndex, leaves) {
+    console.log('generating merkle proof');
+    let tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves);
+    const { pathElements, pathIndices } = tree.path(leafIndex);
+    return { pathElements, pathIndices, root: tree.root() }
+}
+
+async function generateProof({ deposit, recipient, leaves }) {
+    const leafIndex = leaves.indexOf(toHex(deposit.commitment));
+    const { root, pathElements, pathIndices } = await generateMerkleProof(deposit, leafIndex, leaves);
+
+    const input = {
+        // Public snark inputs
+        root: root,
+        nullifierHash: deposit.nullifierHash,
+        recipient: bigInt(recipient),
+        relayer: bigInt(0),
+        fee: bigInt(0),
+        refund: bigInt(0),
+
+        // Private snark inputs
+        nullifier: deposit.nullifier,
+        secret: deposit.secret,
+        pathElements: pathElements,
+        pathIndices: pathIndices,
+    }
+
+    console.log('Generating SNARK proof');
+    console.time('Proof time');
+    const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key);
+    const { proof } = websnarkUtils.toSolidityInput(proofData);
+    console.timeEnd('Proof time');
+
+    const args = [
+        toHex(input.root),
+        toHex(input.nullifierHash),
+        toHex(input.recipient),
+        toHex(input.relayer, 20),
+        toHex(input.fee),
+        toHex(input.refund),
+    ]
+
+    // console.log(args); // uncomment for debug
+    return { proof: proofToLE(proof), args }
+}
+
+export async function withdraw(to, noteString, leaves) {
+    // Substrate 32 byte addrs aren't necessarily within the finite field (as opposed to ETH addresses).
+    // This hack naturally makes it work regardless. Maybe it would even be fine in production too.
+    const recipient = to % PRIME_FIELD;
+    const parsed_note = parseNote(noteString);
+    return await generateProof({ deposit: parsed_note.deposit, recipient, leaves });
+}

+ 11 - 4
integration/substrate/tsconfig.json

@@ -6,14 +6,14 @@
     "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
     "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
     // "lib": [],                             /* Specify library files to be included in the compilation. */
-    // "allowJs": true,                       /* Allow javascript files to be compiled. */
-    // "checkJs": true,                       /* Report errors in .js files. */
+    "allowJs": true, /* Allow javascript files to be compiled. */
+    //"checkJs": true, /* Report errors in .js files. */
     // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
     // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
     // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
     // "sourceMap": true,                     /* Generates corresponding '.map' file. */
     // "outFile": "./",                       /* Concatenate and emit output to single file. */
-    // "outDir": "./",                        /* Redirect output structure to the directory. */
+    "outDir": "./build", /* Redirect output structure to the directory. */
     // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
     // "composite": true,                     /* Enable project compilation */
     // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
@@ -58,5 +58,12 @@
     /* Advanced Options */
     "skipLibCheck": true, /* Skip type checking of declaration files. */
     "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
-  }
+  },
+  "include": [
+    "./*.ts"
+  ],
+  "exclude": [
+    "node_modules",
+    "tornado/tornado-cli"
+  ]
 }