This commit is contained in:
poma 2020-04-08 12:41:12 +03:00
commit 33dddba57e
No known key found for this signature in database
GPG Key ID: BA20CB01FE165657
11 changed files with 4425 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
build

View File

@ -0,0 +1,54 @@
include "../node_modules/circomlib/circuits/mimcsponge.circom";
// Computes MiMC([left, right])
template HashLeftRight() {
signal input left;
signal input right;
signal output hash;
component hasher = MiMCSponge(2, 220, 1);
hasher.ins[0] <== left;
hasher.ins[1] <== right;
hasher.k <== 0;
hash <== hasher.outs[0];
}
// if s == 0 returns [in[0], in[1]]
// if s == 1 returns [in[1], in[0]]
template DualMux() {
signal input in[2];
signal input s;
signal output out[2];
s * (1 - s) === 0
out[0] <== (in[1] - in[0])*s + in[0];
out[1] <== (in[0] - in[1])*s + in[1];
}
// Verifies that merkle proof is correct for given merkle root and a leaf
// pathIndices input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path
template MerkleTree(levels) {
signal input leaf;
signal input root;
signal input pathElements[levels];
signal input pathIndices;
component selectors[levels];
component hashers[levels];
component indexBits = Num2Bits(levels);
indexBits.in <== pathIndices;
for (var i = 0; i < levels; i++) {
selectors[i] = DualMux();
selectors[i].in[0] <== i == 0 ? leaf : hashers[i - 1].hash;
selectors[i].in[1] <== pathElements[i];
selectors[i].s <== indexBits.out[i];
hashers[i] = HashLeftRight();
hashers[i].left <== selectors[i].out[0];
hashers[i].right <== selectors[i].out[1];
}
root === hashers[levels - 1].hash;
}

123
circuits/transaction.circom Normal file
View File

@ -0,0 +1,123 @@
include "./merkleTree.circom"
include "./treeUpdater.circom"
include "./utils.circom"
/*
Utxo structure:
{
amount,
blinding, // random number
pubkey,
}
commitment = hash(amount, blinding, pubKey)
nullifier = hash(commitment, privKey, merklePath)
*/
// Universal JoinSplit transaction with 2 inputs and 2 outputs
template Transaction(levels, zeroLeaf) {
signal input root;
signal input newRoot;
signal input inputNullifier[2];
signal input outputCommitment[2];
// external amount used for deposits and withdrawals
// correct extAmount range is enforced on the smart contract
signal input extAmount;
signal input fee;
signal input recipient;
signal input relayer;
signal private input privateKey;
// data for 2 transaction inputs
signal private input inAmount[2];
signal private input inBlinding[2];
signal private input inPathIndices[2];
signal private input inPathElements[2][levels];
// data for 2 transaction outputs
signal private input outAmount[2];
signal private input outBlinding[2];
signal private input outPathIndices;
signal private input outPathElements[levels - 1];
component inUtxoHasher[2];
component outUtxoHasher[2];
component nullifierHasher[2];
component checkRoot[2]
component tree[2];
component inAmountCheck[2];
component outAmountCheck[2];
component keypair = Keypair();
keypair.privateKey <== privateKey;
// verify correctness of transaction inputs
for (var tx = 0; tx < 2; tx++) {
inUtxoHasher[tx] = TransactionHasher();
inUtxoHasher[tx].amount <== inAmount[tx];
inUtxoHasher[tx].blinding <== inBlinding[tx];
inUtxoHasher[tx].publicKey <== keypair.publicKey;
nullifierHasher[tx] = NullifierHasher();
nullifierHasher[tx].commitment <== inUtxoHasher[tx].commitment;
nullifierHasher[tx].merklePath <== inPathIndices[tx];
nullifierHasher[tx].privateKey <== keypair.privateKey;
nullifierHasher[tx].nullifier === inputNullifier[tx];
tree[tx] = MerkleTree(levels);
tree[tx].leaf <== inUtxoHasher[tx].commitment;
tree[tx].pathIndices <== inPathIndices[tx];
for (var i = 0; i < levels; i++) {
tree[tx].pathElements[i] <== inPathElements[tx][i];
}
// check merkle proof only if amount is non-zero
checkRoot[tx] = ForceEqualIfEnabled();
checkRoot[tx].in[0] <== root;
checkRoot[tx].in[1] <== tree[tx].root;
checkRoot[tx].enabled <== inAmount[tx];
// Check that amount fits into 248 bits to prevent overflow
inAmountCheck[tx] = Num2Bits(248);
inAmountCheck[tx].in <== inAmount[tx];
}
// verify correctness of transaction outputs
for (var tx = 0; tx < 2; tx++) {
outUtxoHasher[tx] = TransactionHasher();
outUtxoHasher[tx].amount <== outAmount[tx];
outUtxoHasher[tx].blinding <== outBlinding[tx];
outUtxoHasher[tx].publicKey <== keypair.publicKey;
outUtxoHasher[tx].commitment === outputCommitment[tx];
// Check that amount fits into 248 bits to prevent overflow
outAmountCheck[tx] = Num2Bits(248);
outAmountCheck[tx].in <== outAmount[tx];
}
// Check that fee fits into 248 bits to prevent overflow
component feeCheck = Num2Bits(248);
feeCheck.in <== fee;
component sameNullifiers = IsEqual();
sameNullifiers.in[0] <== inputNullifier[0];
sameNullifiers.in[1] <== inputNullifier[1];
sameNullifiers.out === 0;
// verify amount invariant
inAmount[0] + inAmount[1] + extAmount === outAmount[0] + outAmount[1] + fee;
// Check merkle tree update with inserted transaction outputs
component treeUpdater = TreeUpdater(levels, zeroLeaf);
treeUpdater.oldRoot <== root;
treeUpdater.newRoot <== newRoot;
treeUpdater.leaf[0] <== outputCommitment[0];
treeUpdater.leaf[1] <== outputCommitment[1];
treeUpdater.pathIndices <== outPathIndices;
for (var i = 0; i < levels - 1; i++) {
treeUpdater.pathElements[i] <== outPathElements[i];
}
}
component main = Transaction(20, 3193090221241211970002919215846211184824251841300455796635909287157453409439);

View File

@ -0,0 +1,32 @@
include "./merkleTree.circom";
// inserts a pair of leaves into a tree
// checks that tree previously contained zeroes is same positions
// zeroLeaf is a second level leaf: `hash(0, 0)`
template TreeUpdater(n, zeroLeaf) {
signal input oldRoot;
signal input newRoot;
signal input leaf[2];
signal input pathIndices;
signal private input pathElements[n - 1];
component leafPair = HashLeftRight();
leafPair.left <== leaf[0];
leafPair.right <== leaf[1];
component treeBefore = MerkleTree(n - 1);
for(var i = 0; i < n - 1; i++) {
treeBefore.pathElements[i] <== pathElements[i];
}
treeBefore.pathIndices <== pathIndices;
treeBefore.leaf <== zeroLeaf;
treeBefore.root === oldRoot;
component treeAfter = MerkleTree(n - 1);
for(var i = 0; i < n - 1; i++) {
treeAfter.pathElements[i] <== pathElements[i];
}
treeAfter.pathIndices <== pathIndices;
treeAfter.leaf <== leafPair.hash;
treeAfter.root === newRoot;
}

44
circuits/utils.circom Normal file
View File

@ -0,0 +1,44 @@
include "../node_modules/circomlib/circuits/pointbits.circom";
include "../node_modules/circomlib/circuits/compconstant.circom";
include "../node_modules/circomlib/circuits/mimcsponge.circom";
template Keypair() {
signal input privateKey;
signal output publicKey;
publicKey <== privateKey;
// todo
}
template TransactionHasher() {
signal input amount;
signal input blinding;
signal input publicKey;
signal output commitment;
component hasher = MiMCSponge(3, 220, 1);
hasher.ins[0] <== amount;
hasher.ins[1] <== blinding;
hasher.ins[2] <== publicKey;
hasher.k <== 0;
commitment <== hasher.outs[0];
}
template NullifierHasher() {
signal input privateKey;
signal input merklePath;
signal input commitment;
signal output nullifier;
component hasher = MiMCSponge(3, 220, 1);
hasher.ins[0] <== commitment;
hasher.ins[1] <== merklePath;
hasher.ins[2] <== privateKey;
hasher.k <== 0;
nullifier <== hasher.outs[0];
}

123
contracts/Tornado.sol Normal file
View File

@ -0,0 +1,123 @@
// 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
*/
pragma solidity ^0.5.8;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; // todo: maybe remove?
contract IVerifier {
function verifyProof(bytes memory _proof, uint256[10] memory _input) public returns(bool);
}
contract TornadoPool is ReentrancyGuard {
uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
uint256 public constant MAX_EXT_AMOUNT = 2**248 - 1;
mapping(bytes32 => bool) public nullifierHashes;
bytes32 public currentRoot;
IVerifier public verifier;
// todo: event Transaction();
event NewCommitment(bytes32 commitment);
event NewNullifier(bytes32 nullifier);
/**
@dev The constructor
@param _verifier the address of SNARK verifier for this contract
*/
constructor(IVerifier _verifier) public {
verifier = _verifier;
}
function transaction(
bytes calldata _proof,
bytes32 _root,
bytes32 _newRoot,
bytes32[2] calldata _inputNullifiers,
bytes32[2] calldata _outputCommitments,
uint256 _extAmount,
uint256 _fee,
address payable _recipient,
address payable _relayer
)
external payable nonReentrant
{
require(currentRoot == _root, "Invalid merkle root");
require(!isSpent(_inputNullifiers[0]), "Input 0 is already spent");
require(!isSpent(_inputNullifiers[1]), "Input 1 is already spent");
require(verifier.verifyProof(_proof, [
uint256(_root),
uint256(_newRoot),
uint256(_inputNullifiers[0]),
uint256(_inputNullifiers[1]),
uint256(_outputCommitments[0]),
uint256(_outputCommitments[1]),
_extAmount,
_fee,
uint256(_relayer),
uint256(_recipient)
]), "Invalid transaction proof");
currentRoot = _newRoot;
nullifierHashes[_inputNullifiers[0]] = true;
nullifierHashes[_inputNullifiers[1]] = true;
int256 extAmount = calculateExternalAmount(_extAmount);
if (extAmount > 0) {
require(msg.value == uint256(extAmount), "Incorrect amount of ETH sent on deposit");
} else {
require(msg.value == 0, "Sent ETH amount should be 0 for withdrawal");
transfer(_recipient, uint256(-extAmount));
}
if (_fee > 0) {
transfer(_relayer, _fee);
}
emit NewCommitment(_outputCommitments[0]);
emit NewCommitment(_outputCommitments[1]);
emit NewNullifier(_inputNullifiers[0]);
emit NewNullifier(_inputNullifiers[1]);
// emit Transaction();
}
function calculateExternalAmount(uint256 _extAmount) public pure returns(int256) {
// -MAX_EXT_AMOUNT < extAmount < MAX_EXT_AMOUNT
if (_extAmount < MAX_EXT_AMOUNT) {
return int256(_extAmount);
} else if (_extAmount > FIELD_SIZE - MAX_EXT_AMOUNT) {
// FIELD_SIZE - MAX_EXT_AMOUNT < _extAmount < FIELD_SIZE
return -(int256(FIELD_SIZE) - int256(_extAmount));
} else {
revert("Invalid extAmount value");
}
}
function transfer(address payable to, uint256 amount) internal {
(bool success, ) = to.call.value(amount)("");
require(success, "payment did not go through");
}
/** @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(uint i = 0; i < _nullifierHashes.length; i++) {
if (isSpent(_nullifierHashes[i])) {
spent[i] = true;
}
}
}
}

View File

@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};

3915
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "tornado-pool",
"version": "1.0.0",
"description": "",
"main": "truffle-config.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@openzeppelin/contracts": "^2.5.0",
"circomlib": "0.0.21"
}
}

99
truffle-config.js Normal file
View File

@ -0,0 +1,99 @@
/**
* Use this file to configure your truffle project. It's seeded with some
* common settings for different networks and features like migrations,
* compilation and testing. Uncomment the ones you need or modify
* them to suit your project as necessary.
*
* More information about configuration can be found at:
*
* truffleframework.com/docs/advanced/configuration
*
* To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider)
* to sign your transactions before they're sent to a remote public node. Infura accounts
* are available for free at: infura.io/register.
*
* You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
* public/private key pairs. If you're publishing your code to GitHub make sure you load this
* phrase from a file you've .gitignored so it doesn't accidentally become public.
*
*/
// const HDWalletProvider = require('@truffle/hdwallet-provider');
// const infuraKey = "fj4jll3k.....";
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();
module.exports = {
/**
* Networks define how you connect to your ethereum client and let you set the
* defaults web3 uses to send transactions. If you don't specify one truffle
* will spin up a development blockchain for you on port 9545 when you
* run `develop` or `test`. You can ask a truffle command to use a specific
* network from the command line, e.g
*
* $ truffle test --network <network-name>
*/
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
// development: {
// host: "127.0.0.1", // Localhost (default: none)
// port: 8545, // Standard Ethereum port (default: none)
// network_id: "*", // Any network (default: none)
// },
// Another network with more advanced options...
// advanced: {
// port: 8777, // Custom port
// network_id: 1342, // Custom network
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
// from: <address>, // Account to send txs from (default: accounts[0])
// websockets: true // Enable EventEmitter interface for web3 (default: false)
// },
// Useful for deploying to a public network.
// NB: It's important to wrap the provider as a function.
// ropsten: {
// provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
// network_id: 3, // Ropsten's id
// gas: 5500000, // Ropsten has a lower block limit than mainnet
// confirmations: 2, // # of confs to wait between deployments. (default: 0)
// timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
// skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
// },
// Useful for private networks
// private: {
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
// network_id: 2111, // This network is yours, in the cloud.
// production: true // Treats this network as if it was a public net. (default: false)
// }
},
// Set default mocha options here, use special reporters etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
// version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
// optimizer: {
// enabled: false,
// runs: 200
// },
// evmVersion: "byzantium"
// }
}
}
}