mirror of
https://github.com/tornadocash/tornado-nova
synced 2024-02-02 14:53:56 +01:00
initial
This commit is contained in:
commit
33dddba57e
9
.editorconfig
Normal file
9
.editorconfig
Normal 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
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
build
|
54
circuits/merkleTree.circom
Normal file
54
circuits/merkleTree.circom
Normal 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
123
circuits/transaction.circom
Normal 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);
|
32
circuits/treeUpdater.circom
Normal file
32
circuits/treeUpdater.circom
Normal 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
44
circuits/utils.circom
Normal 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
123
contracts/Tornado.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
migrations/1_initial_migration.js
Normal file
5
migrations/1_initial_migration.js
Normal file
@ -0,0 +1,5 @@
|
||||
const Migrations = artifacts.require("Migrations");
|
||||
|
||||
module.exports = function(deployer) {
|
||||
deployer.deploy(Migrations);
|
||||
};
|
3915
package-lock.json
generated
Normal file
3915
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal 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
99
truffle-config.js
Normal 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"
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user