This commit is contained in:
Alexey 2021-06-07 13:12:15 +03:00
parent 516d599502
commit 238431233b
7 changed files with 116 additions and 78 deletions

15
TODO
View File

@ -1,4 +1,11 @@
* update verifier version * shuffle outputs and inputs
* shuffle outputs * utxo data encryption for recipient as for mining
* utxo data encryption for recipient - combine privkey hash and ethereum public key = address
* poseidon * outputs merging
* switch web3 to etherjs
* tests
* wasmsnark
* ERC20?
* design
* relayer
* race condition ?

View File

@ -24,8 +24,7 @@ template Transaction(levels, zeroLeaf) {
// correct extAmount range is enforced on the smart contract // correct extAmount range is enforced on the smart contract
signal input extAmount; signal input extAmount;
signal input fee; signal input fee;
signal input recipient; signal input extDataHash;
signal input relayer;
signal private input privateKey; signal private input privateKey;
@ -121,4 +120,6 @@ template Transaction(levels, zeroLeaf) {
} }
} }
// zeroLeaf = Poseidon(zero, zero)
// default `zero` value is keccak256("tornado") % FIELD_SIZE = 21663839004416932945382355908790599225266501822907911457504978515578255421292
component main = Transaction(5, 11850551329423159860688778991827824730037759162201783566284850822760196767874); component main = Transaction(5, 11850551329423159860688778991827824730037759162201783566284850822760196767874);

View File

@ -11,11 +11,12 @@
*/ */
pragma solidity ^0.6.0; pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; // todo: maybe remove? import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; // todo: maybe remove?
interface IVerifier { interface IVerifier {
function verifyProof(bytes memory _proof, uint256[10] memory _input) external returns(bool); function verifyProof(bytes memory _proof, uint256[9] memory _input) external returns(bool);
} }
contract TornadoPool is ReentrancyGuard { contract TornadoPool is ReentrancyGuard {
@ -27,8 +28,15 @@ contract TornadoPool is ReentrancyGuard {
uint public currentCommitmentIndex; uint public currentCommitmentIndex;
IVerifier public verifier; IVerifier public verifier;
struct ExtData {
address payable _recipient;
address payable _relayer;
bytes encryptedOutput1;
bytes encryptedOutput2;
}
// todo: event Transaction(); // todo: event Transaction();
event NewCommitment(bytes32 commitment, uint index); event NewCommitment(bytes32 commitment, uint index, bytes encryptedOutput);
event NewNullifier(bytes32 nullifier); event NewNullifier(bytes32 nullifier);
event Withdraw(bytes32 indexed nullifier); // todo emit it on withdraw so we can easily find the withdraw tx for user on UI event Withdraw(bytes32 indexed nullifier); // todo emit it on withdraw so we can easily find the withdraw tx for user on UI
@ -49,14 +57,15 @@ contract TornadoPool is ReentrancyGuard {
bytes32[2] calldata _outputCommitments, bytes32[2] calldata _outputCommitments,
uint256 _extAmount, uint256 _extAmount,
uint256 _fee, uint256 _fee,
address payable _recipient, ExtData calldata _extData,
address payable _relayer bytes32 _extDataHash
) )
external payable nonReentrant external payable nonReentrant
{ {
require(currentRoot == _root, "Invalid merkle root"); require(currentRoot == _root, "Invalid merkle root");
require(!isSpent(_inputNullifiers[0]), "Input 0 is already spent"); require(!isSpent(_inputNullifiers[0]), "Input 0 is already spent");
require(!isSpent(_inputNullifiers[1]), "Input 1 is already spent"); require(!isSpent(_inputNullifiers[1]), "Input 1 is already spent");
require(uint256(_extDataHash) == uint256(keccak256(abi.encode(_extData))) % FIELD_SIZE, "Incorrect external data hash");
require(verifier.verifyProof(_proof, [ require(verifier.verifyProof(_proof, [
uint256(_root), uint256(_root),
uint256(_newRoot), uint256(_newRoot),
@ -66,8 +75,7 @@ contract TornadoPool is ReentrancyGuard {
uint256(_outputCommitments[1]), uint256(_outputCommitments[1]),
_extAmount, _extAmount,
_fee, _fee,
uint256(_recipient), uint256(_extDataHash)
uint256(_relayer)
]), "Invalid transaction proof"); ]), "Invalid transaction proof");
currentRoot = _newRoot; currentRoot = _newRoot;
@ -79,16 +87,16 @@ contract TornadoPool is ReentrancyGuard {
require(msg.value == uint256(extAmount), "Incorrect amount of ETH sent on deposit"); require(msg.value == uint256(extAmount), "Incorrect amount of ETH sent on deposit");
} else { } else {
require(msg.value == 0, "Sent ETH amount should be 0 for withdrawal"); require(msg.value == 0, "Sent ETH amount should be 0 for withdrawal");
_recipient.transfer(uint256(-extAmount)); _extData._recipient.transfer(uint256(-extAmount));
} }
if (_fee > 0) { if (_fee > 0) {
_recipient.transfer(_fee); _extData._relayer.transfer(_fee);
} }
// todo enforce currentCommitmentIndex value in snark // todo enforce currentCommitmentIndex value in snark
emit NewCommitment(_outputCommitments[0], currentCommitmentIndex++); emit NewCommitment(_outputCommitments[0], currentCommitmentIndex++, _extData.encryptedOutput1);
emit NewCommitment(_outputCommitments[1], currentCommitmentIndex++); emit NewCommitment(_outputCommitments[1], currentCommitmentIndex++, _extData.encryptedOutput2);
emit NewNullifier(_inputNullifiers[0]); emit NewNullifier(_inputNullifiers[0]);
emit NewNullifier(_inputNullifiers[1]); emit NewNullifier(_inputNullifiers[1]);
// emit Transaction(); // emit Transaction();
@ -110,14 +118,4 @@ contract TornadoPool is ReentrancyGuard {
function isSpent(bytes32 _nullifierHash) public view returns(bool) { function isSpent(bytes32 _nullifierHash) public view returns(bool) {
return nullifierHashes[_nullifierHash]; 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

@ -7,9 +7,9 @@
"test": "test" "test": "test"
}, },
"scripts": { "scripts": {
"build:circuit": "./scripts/buildCircuit.sh transaction", "circuit": "./scripts/buildCircuit.sh transaction",
"build:contract": "npx hardhat compile", "compile": "npx hardhat compile",
"build": "npm run build:circuit && npm run build:contract", "build": "npm run circuit && npm run compile",
"migrate": "npx hardhat run scripts/deploy.js --network localhost", "migrate": "npx hardhat run scripts/deploy.js --network localhost",
"start": "node ./src/index.js" "start": "node ./src/index.js"
}, },

View File

@ -1,8 +1,5 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
const MerkleTree = require('fixed-merkle-tree') const MerkleTree = require('fixed-merkle-tree')
const fs = require('fs')
const crypto = require('crypto')
const { poseidon } = require('circomlib')
const Web3 = require('web3') const Web3 = require('web3')
const { ethers } = require('hardhat') const { ethers } = require('hardhat')
const { BigNumber } = ethers const { BigNumber } = ethers
@ -85,64 +82,86 @@ async function insertOutput(tree, output) {
output.merklePathElements = pathElements output.merklePathElements = pathElements
} }
async function deposit() { async function getProof({ input1, input2, output1, output2, tree, extAmount, fee, recipient, relayer }) {
const amount = 1e6 const oldRoot = tree.root()
const tree = await buildMerkleTree()
const oldRoot = await tree.root()
const keypair = randomKeypair()
const tx = createDeposit(amount, keypair)
await insertOutput(tree, tx.outputs[0])
await insertOutput(tree, tx.outputs[1])
console.log('Note', tx.outputs[0])
let input = { // if deposit require(extAmount > 0)
root: oldRoot,
newRoot: await tree.root(),
inputNullifier: [tx.inputs[0].nullifier, tx.inputs[1].nullifier],
outputCommitment: [tx.outputs[0].commitment, tx.outputs[1].commitment],
extAmount: amount,
fee: 0,
recipient: 0,
relayer: 0,
// private inputs if (input1.amount !== 0) {
privateKey: tx.inputs[0].privkey, // transact
const index1 = await tree.indexOf(toFixedHex(input1.commitment))
// data for 2 transaction inputs const path1 = await tree.path(index1)
inAmount: [tx.inputs[0].amount, tx.inputs[1].amount],
inBlinding: [tx.inputs[0].blinding, tx.inputs[1].blinding],
inPathIndices: [
bitsToNumber(tx.inputs[0].merklePathIndices),
bitsToNumber(tx.inputs[1].merklePathIndices),
],
inPathElements: [tx.inputs[0].merklePathElements, tx.inputs[1].merklePathElements],
// data for 2 transaction outputs
outAmount: [tx.outputs[0].amount, tx.outputs[1].amount],
outBlinding: [tx.outputs[0].blinding, tx.outputs[1].blinding],
outPubkey: [tx.outputs[0].pubkey, tx.outputs[1].pubkey],
outPathIndices: bitsToNumber(tx.outputs[0].merklePathIndices.slice(1)),
outPathElements: tx.outputs[0].merklePathElements.slice(1),
} }
// console.log('input', JSON.stringify(stringifyBigInts(input))) await insertOutput(tree, output1)
console.log('DEPOSIT input', input) await insertOutput(tree, output2)
extData = recipient + relayer // TODO
let input = {
root: oldRoot,
newRoot: tree.root(),
inputNullifier: [input1.nullifier, input2.nullifier],
outputCommitment: [outputs1.commitment, outputs2.commitment],
extAmount,
fee,
extData,
// private inputs
privateKey: inputs1.privkey, // TODO make sure you use the right one when you shuffle inputs
// data for 2 transaction inputs
inAmount: [inputs1.amount, inputs2.amount],
inBlinding: [inputs1.blinding, inputs2.blinding],
inPathIndices: [bitsToNumber(inputs1.merklePathIndices), bitsToNumber(inputs2.merklePathIndices)],
inPathElements: [inputs1.merklePathElements, inputs2.merklePathElements],
// data for 2 transaction outputs
outAmount: [outputs1.amount, outputs2.amount],
outBlinding: [outputs1.blinding, outputs2.blinding],
outPubkey: [outputs1.pubkey, outputs2.pubkey],
outPathIndices: bitsToNumber[outputs1.merklePathIndices.slice(1)],
outPathElements: [outputs1.merklePathElements.slice(1)],
}
console.log('SNARK input', input)
console.log('Generating SNARK proof...') console.log('Generating SNARK proof...')
const proof = await prove(input, './artifacts/circuits/transaction') const proof = await prove(input, './artifacts/circuits/transaction')
const args = [ const args = [
toFixedHex(input.root), toFixedHex(input.root),
toFixedHex(input.newRoot), toFixedHex(input.newRoot),
[toFixedHex(tx.inputs[0].nullifier), toFixedHex(tx.inputs[1].nullifier)], [toFixedHex(input1.nullifier), toFixedHex(input2.nullifier)],
[toFixedHex(tx.outputs[0].commitment), toFixedHex(tx.outputs[1].commitment)], [toFixedHex(output1.commitment), toFixedHex(output2.commitment)],
toFixedHex(amount), toFixedHex(0),
toFixedHex(input.fee), toFixedHex(input.fee),
toFixedHex(input.recipient, 20), toFixedHex(extData), // extData hash actually
toFixedHex(input.relayer, 20),
] ]
return {
proof,
args,
}
}
async function deposit() {
const amount = 1e6
const tree = await buildMerkleTree()
const keypair = randomKeypair()
const tx = createDeposit(amount, keypair)
const { proof, args } = await getProof({
input1: tx.inputs[0],
input2: tx.inputs[1],
output1: tx.outputs[1],
output2: tx.outputs[1],
tree,
extAmount: amount,
fee: 0,
recipient: 0,
relayer: 0,
})
console.log('Sending deposit transaction...') console.log('Sending deposit transaction...')
const receipt = await contract.methods const receipt = await contract.methods
.transaction(proof, ...args) .transaction(proof, ...args)

View File

@ -6,8 +6,6 @@ const tmp = require('tmp-promise')
const util = require('util') const util = require('util')
const exec = util.promisify(require('child_process').exec) const exec = util.promisify(require('child_process').exec)
function stringify() {}
function prove(input, keyBasePath) { function prove(input, keyBasePath) {
input = utils.stringifyBigInts(input) input = utils.stringifyBigInts(input)
console.log('input', input) console.log('input', input)

View File

@ -6,9 +6,22 @@ const { poseidon } = require('circomlib')
const poseidonHash = (items) => BigNumber.from(poseidon(items).toString()) const poseidonHash = (items) => BigNumber.from(poseidon(items).toString())
const poseidonHash2 = (a, b) => poseidonHash([a, b]) const poseidonHash2 = (a, b) => poseidonHash([a, b])
const FIELD_SIZE = BigNumber.from(
'21888242871839275222246405745257275088548364400416034343698204186575808495617',
)
/** Generate random number of specified byte length */ /** Generate random number of specified byte length */
const randomBN = (nbytes = 31) => BigNumber.from(crypto.randomBytes(nbytes)) const randomBN = (nbytes = 31) => BigNumber.from(crypto.randomBytes(nbytes))
function getExtDataHash({ recipient, relayer, encryptedOutput1, encryptedOutput2 }) {
const encodedData = abi.encode(
['address', 'address', 'bytes', 'bytes'],
[toFixedHex(recipient, 20), toFixedHex(relayer, 20), encryptedOutput1, encryptedOutput2],
)
const hash = ethers.utils.keccak256(encodedData)
return BigNumber.from(hash).mod(FIELD_SIZE)
}
/** BigNumber to hex string of specified length */ /** BigNumber to hex string of specified length */
const toFixedHex = (number, length = 32) => const toFixedHex = (number, length = 32) =>
'0x' + '0x' +
@ -35,10 +48,12 @@ function bitsToNumber(bits) {
} }
module.exports = { module.exports = {
FIELD_SIZE,
randomBN, randomBN,
bitsToNumber, bitsToNumber,
toFixedHex, toFixedHex,
toBuffer, toBuffer,
poseidonHash, poseidonHash,
poseidonHash2, poseidonHash2,
getExtDataHash,
} }