mirror of
https://github.com/tornadocash/tornado-nova
synced 2024-02-02 14:53:56 +01:00
Merge branch 'master' into optimism
This commit is contained in:
commit
8842501702
@ -7,3 +7,7 @@ yarn
|
|||||||
yarn build
|
yarn build
|
||||||
yarn test
|
yarn test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
1. deposit from mainnet to the pool on optimism in one tx
|
||||||
|
@ -18,10 +18,10 @@ nullifier = hash(commitment, privKey, merklePath)
|
|||||||
template Transaction(levels, nIns, nOuts, zeroLeaf) {
|
template Transaction(levels, nIns, nOuts, zeroLeaf) {
|
||||||
signal input root;
|
signal input root;
|
||||||
signal input newRoot;
|
signal input newRoot;
|
||||||
// external amount used for deposits and withdrawals
|
// extAmount = external amount used for deposits and withdrawals
|
||||||
// correct extAmount range is enforced on the smart contract
|
// correct extAmount range is enforced on the smart contract
|
||||||
signal input extAmount;
|
// publicAmount = fee - extAmount
|
||||||
signal input fee;
|
signal input publicAmount;
|
||||||
signal input extDataHash;
|
signal input extDataHash;
|
||||||
|
|
||||||
// data for transaction inputs
|
// data for transaction inputs
|
||||||
@ -103,10 +103,6 @@ template Transaction(levels, nIns, nOuts, zeroLeaf) {
|
|||||||
sumOuts += outAmount[tx];
|
sumOuts += outAmount[tx];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that fee fits into 248 bits to prevent overflow
|
|
||||||
component feeCheck = Num2Bits(248);
|
|
||||||
feeCheck.in <== fee;
|
|
||||||
|
|
||||||
// check that there are no same nullifiers among all inputs
|
// check that there are no same nullifiers among all inputs
|
||||||
component sameNullifiers[nIns * (nIns - 1) / 2];
|
component sameNullifiers[nIns * (nIns - 1) / 2];
|
||||||
var index = 0;
|
var index = 0;
|
||||||
@ -121,10 +117,10 @@ template Transaction(levels, nIns, nOuts, zeroLeaf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// verify amount invariant
|
// verify amount invariant
|
||||||
sumIns + extAmount === sumOuts + fee;
|
sumIns + publicAmount === sumOuts;
|
||||||
|
|
||||||
// Check merkle tree update with inserted transaction outputs
|
// Check merkle tree update with inserted transaction outputs
|
||||||
component treeUpdater = TreeUpdater(levels, zeroLeaf);
|
component treeUpdater = TreeUpdater(levels, 1 /* log2(nOuts) */, zeroLeaf);
|
||||||
treeUpdater.oldRoot <== root;
|
treeUpdater.oldRoot <== root;
|
||||||
treeUpdater.newRoot <== newRoot;
|
treeUpdater.newRoot <== newRoot;
|
||||||
for (var i = 0; i < nOuts; i++) {
|
for (var i = 0; i < nOuts; i++) {
|
||||||
|
@ -1,29 +1,36 @@
|
|||||||
include "./merkleTree.circom";
|
include "./merkleTree.circom";
|
||||||
|
|
||||||
// inserts a pair of leaves into a tree
|
// inserts a subtree into a merkle tree
|
||||||
// checks that tree previously contained zeroes is same positions
|
// checks that tree previously contained zeroes is the same positions
|
||||||
// zeroLeaf is a second level leaf: `hash(0, 0)`
|
// zeroSubtreeRoot is a root of a subtree that contains only zeroes
|
||||||
template TreeUpdater(n, zeroLeaf) {
|
template TreeUpdater(levels, subtreeLevels, zeroSubtreeRoot) {
|
||||||
|
// currently it works only with 1-level subtrees
|
||||||
|
assert(subtreeLevels == 1);
|
||||||
|
var remainingLevels = levels - subtreeLevels;
|
||||||
|
|
||||||
signal input oldRoot;
|
signal input oldRoot;
|
||||||
signal input newRoot;
|
signal input newRoot;
|
||||||
signal input leaf[2];
|
signal input leaf[1 << subtreeLevels];
|
||||||
signal input pathIndices;
|
signal input pathIndices;
|
||||||
signal private input pathElements[n - 1];
|
signal private input pathElements[remainingLevels];
|
||||||
|
|
||||||
|
// calculate subtree root
|
||||||
|
// todo: make it work with arbitrary subtree levels
|
||||||
|
// currently it works only with 1-level subtrees
|
||||||
component leafPair = HashLeftRight();
|
component leafPair = HashLeftRight();
|
||||||
leafPair.left <== leaf[0];
|
leafPair.left <== leaf[0];
|
||||||
leafPair.right <== leaf[1];
|
leafPair.right <== leaf[1];
|
||||||
|
|
||||||
component treeBefore = MerkleTree(n - 1);
|
component treeBefore = MerkleTree(remainingLevels);
|
||||||
for(var i = 0; i < n - 1; i++) {
|
for(var i = 0; i < remainingLevels; i++) {
|
||||||
treeBefore.pathElements[i] <== pathElements[i];
|
treeBefore.pathElements[i] <== pathElements[i];
|
||||||
}
|
}
|
||||||
treeBefore.pathIndices <== pathIndices;
|
treeBefore.pathIndices <== pathIndices;
|
||||||
treeBefore.leaf <== zeroLeaf;
|
treeBefore.leaf <== zeroSubtreeRoot;
|
||||||
treeBefore.root === oldRoot;
|
treeBefore.root === oldRoot;
|
||||||
|
|
||||||
component treeAfter = MerkleTree(n - 1);
|
component treeAfter = MerkleTree(remainingLevels);
|
||||||
for(var i = 0; i < n - 1; i++) {
|
for(var i = 0; i < remainingLevels; i++) {
|
||||||
treeAfter.pathElements[i] <== pathElements[i];
|
treeAfter.pathElements[i] <== pathElements[i];
|
||||||
}
|
}
|
||||||
treeAfter.pathIndices <== pathIndices;
|
treeAfter.pathIndices <== pathIndices;
|
||||||
|
@ -15,9 +15,9 @@ pragma experimental ABIEncoderV2;
|
|||||||
import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
|
import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
|
||||||
|
|
||||||
interface IVerifier {
|
interface IVerifier {
|
||||||
function verifyProof(bytes memory _proof, uint256[10] memory _input) external view returns (bool);
|
function verifyProof(bytes memory _proof, uint256[9] memory _input) external view returns (bool);
|
||||||
|
|
||||||
function verifyProof(bytes memory _proof, uint256[24] memory _input) external view returns (bool);
|
function verifyProof(bytes memory _proof, uint256[23] memory _input) external view returns (bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ERC20 {
|
interface ERC20 {
|
||||||
@ -26,7 +26,8 @@ interface ERC20 {
|
|||||||
|
|
||||||
contract TornadoPool is Initializable {
|
contract TornadoPool is Initializable {
|
||||||
uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
|
uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617;
|
||||||
uint256 public constant MAX_EXT_AMOUNT = 2**248 - 1;
|
int256 public constant MAX_EXT_AMOUNT = 2**248;
|
||||||
|
uint256 public constant MAX_FEE = 2**248;
|
||||||
|
|
||||||
mapping(bytes32 => bool) public nullifierHashes;
|
mapping(bytes32 => bool) public nullifierHashes;
|
||||||
bytes32 public currentRoot;
|
bytes32 public currentRoot;
|
||||||
@ -36,7 +37,9 @@ contract TornadoPool is Initializable {
|
|||||||
|
|
||||||
struct ExtData {
|
struct ExtData {
|
||||||
address payable recipient;
|
address payable recipient;
|
||||||
|
int256 extAmount;
|
||||||
address payable relayer;
|
address payable relayer;
|
||||||
|
uint256 fee;
|
||||||
bytes encryptedOutput1;
|
bytes encryptedOutput1;
|
||||||
bytes encryptedOutput2;
|
bytes encryptedOutput2;
|
||||||
}
|
}
|
||||||
@ -48,8 +51,7 @@ contract TornadoPool is Initializable {
|
|||||||
bytes32[] inputNullifiers;
|
bytes32[] inputNullifiers;
|
||||||
bytes32[2] outputCommitments;
|
bytes32[2] outputCommitments;
|
||||||
uint256 outPathIndices;
|
uint256 outPathIndices;
|
||||||
uint256 extAmount;
|
uint256 publicAmount;
|
||||||
uint256 fee;
|
|
||||||
bytes32 extDataHash;
|
bytes32 extDataHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,33 +85,35 @@ contract TornadoPool is Initializable {
|
|||||||
require(!isSpent(_args.inputNullifiers[i]), "Input is already spent");
|
require(!isSpent(_args.inputNullifiers[i]), "Input is already spent");
|
||||||
}
|
}
|
||||||
require(uint256(_args.extDataHash) == uint256(keccak256(abi.encode(_extData))) % FIELD_SIZE, "Incorrect external data hash");
|
require(uint256(_args.extDataHash) == uint256(keccak256(abi.encode(_extData))) % FIELD_SIZE, "Incorrect external data hash");
|
||||||
require(_args.outPathIndices == currentCommitmentIndex >> 1, "Invalid merkle tree insert position");
|
uint256 cachedCommitmentIndex = currentCommitmentIndex;
|
||||||
|
require(_args.outPathIndices == cachedCommitmentIndex >> 1, "Invalid merkle tree insert position");
|
||||||
|
require(_args.publicAmount == calculatePublicAmount(_extData.extAmount, _extData.fee), "Invalid public amount");
|
||||||
require(verifyProof(_args), "Invalid transaction proof");
|
require(verifyProof(_args), "Invalid transaction proof");
|
||||||
|
|
||||||
currentRoot = _args.newRoot;
|
currentRoot = _args.newRoot;
|
||||||
|
currentCommitmentIndex = cachedCommitmentIndex + 2;
|
||||||
for (uint256 i = 0; i < _args.inputNullifiers.length; i++) {
|
for (uint256 i = 0; i < _args.inputNullifiers.length; i++) {
|
||||||
nullifierHashes[_args.inputNullifiers[i]] = true;
|
nullifierHashes[_args.inputNullifiers[i]] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int256 extAmount = calculateExternalAmount(_args.extAmount);
|
if (_extData.extAmount > 0) {
|
||||||
if (extAmount > 0) {
|
require(msg.value == uint256(_extData.extAmount), "Incorrect amount of ETH sent on deposit");
|
||||||
require(msg.value == uint256(_args.extAmount), "Incorrect amount of ETH sent on deposit");
|
} else if (_extData.extAmount < 0) {
|
||||||
} else if (extAmount < 0) {
|
|
||||||
require(msg.value == 0, "Sent ETH amount should be 0 for withdrawal");
|
require(msg.value == 0, "Sent ETH amount should be 0 for withdrawal");
|
||||||
require(_extData.recipient != address(0), "Can't withdraw to zero address");
|
require(_extData.recipient != address(0), "Can't withdraw to zero address");
|
||||||
// _extData.recipient.transfer(uint256(-extAmount));
|
// _extData.recipient.transfer(uint256(-extAmount));
|
||||||
_transfer(_extData.recipient, uint256(-extAmount));
|
_transfer(_extData.recipient, uint256(-_extData.extAmount));
|
||||||
} else {
|
} else {
|
||||||
require(msg.value == 0, "Sent ETH amount should be 0 for transaction");
|
require(msg.value == 0, "Sent ETH amount should be 0 for transaction");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_args.fee > 0) {
|
if (_extData.fee > 0) {
|
||||||
// _extData.relayer.transfer(_args.fee);
|
// _extData.relayer.transfer(_args.fee);
|
||||||
_transfer(_extData.relayer, _args.fee);
|
_transfer(_extData.relayer, _extData.fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit NewCommitment(_args.outputCommitments[0], currentCommitmentIndex++, _extData.encryptedOutput1);
|
emit NewCommitment(_args.outputCommitments[0], cachedCommitmentIndex, _extData.encryptedOutput1);
|
||||||
emit NewCommitment(_args.outputCommitments[1], currentCommitmentIndex++, _extData.encryptedOutput2);
|
emit NewCommitment(_args.outputCommitments[1], cachedCommitmentIndex + 1, _extData.encryptedOutput2);
|
||||||
for (uint256 i = 0; i < _args.inputNullifiers.length; i++) {
|
for (uint256 i = 0; i < _args.inputNullifiers.length; i++) {
|
||||||
emit NewNullifier(_args.inputNullifiers[i]);
|
emit NewNullifier(_args.inputNullifiers[i]);
|
||||||
}
|
}
|
||||||
@ -127,16 +131,11 @@ contract TornadoPool is Initializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateExternalAmount(uint256 _extAmount) public pure returns (int256) {
|
function calculatePublicAmount(int256 _extAmount, uint256 _fee) public pure returns(uint256) {
|
||||||
// -MAX_EXT_AMOUNT < extAmount < MAX_EXT_AMOUNT
|
require(_fee < MAX_FEE, "Invalid fee");
|
||||||
if (_extAmount < MAX_EXT_AMOUNT) {
|
require(_extAmount > -MAX_EXT_AMOUNT && _extAmount < MAX_EXT_AMOUNT, "Invalid ext amount");
|
||||||
return int256(_extAmount);
|
int256 publicAmount = _extAmount - int256(_fee);
|
||||||
} else if (_extAmount > FIELD_SIZE - MAX_EXT_AMOUNT) {
|
return (publicAmount >= 0) ? uint256(publicAmount) : FIELD_SIZE - uint256(-publicAmount);
|
||||||
// FIELD_SIZE - MAX_EXT_AMOUNT < _extAmount < FIELD_SIZE
|
|
||||||
return -(int256(FIELD_SIZE) - int256(_extAmount));
|
|
||||||
} else {
|
|
||||||
revert("Invalid extAmount value");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @dev whether a note is already spent */
|
/** @dev whether a note is already spent */
|
||||||
@ -152,8 +151,7 @@ contract TornadoPool is Initializable {
|
|||||||
[
|
[
|
||||||
uint256(_args.root),
|
uint256(_args.root),
|
||||||
uint256(_args.newRoot),
|
uint256(_args.newRoot),
|
||||||
_args.extAmount,
|
_args.publicAmount,
|
||||||
_args.fee,
|
|
||||||
uint256(_args.extDataHash),
|
uint256(_args.extDataHash),
|
||||||
uint256(_args.inputNullifiers[0]),
|
uint256(_args.inputNullifiers[0]),
|
||||||
uint256(_args.inputNullifiers[1]),
|
uint256(_args.inputNullifiers[1]),
|
||||||
@ -169,8 +167,7 @@ contract TornadoPool is Initializable {
|
|||||||
[
|
[
|
||||||
uint256(_args.root),
|
uint256(_args.root),
|
||||||
uint256(_args.newRoot),
|
uint256(_args.newRoot),
|
||||||
_args.extAmount,
|
_args.publicAmount,
|
||||||
_args.fee,
|
|
||||||
uint256(_args.extDataHash),
|
uint256(_args.extDataHash),
|
||||||
uint256(_args.inputNullifiers[0]),
|
uint256(_args.inputNullifiers[0]),
|
||||||
uint256(_args.inputNullifiers[1]),
|
uint256(_args.inputNullifiers[1]),
|
||||||
|
13
src/index.js
13
src/index.js
@ -48,7 +48,9 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
|
|||||||
|
|
||||||
const extData = {
|
const extData = {
|
||||||
recipient: toFixedHex(recipient, 20),
|
recipient: toFixedHex(recipient, 20),
|
||||||
|
extAmount: toFixedHex(extAmount),
|
||||||
relayer: toFixedHex(relayer, 20),
|
relayer: toFixedHex(relayer, 20),
|
||||||
|
fee: toFixedHex(fee),
|
||||||
encryptedOutput1: outputs[0].encrypt(),
|
encryptedOutput1: outputs[0].encrypt(),
|
||||||
encryptedOutput2: outputs[1].encrypt(),
|
encryptedOutput2: outputs[1].encrypt(),
|
||||||
}
|
}
|
||||||
@ -59,8 +61,7 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
|
|||||||
newRoot: tree.root(),
|
newRoot: tree.root(),
|
||||||
inputNullifier: inputs.map((x) => x.getNullifier()),
|
inputNullifier: inputs.map((x) => x.getNullifier()),
|
||||||
outputCommitment: outputs.map((x) => x.getCommitment()),
|
outputCommitment: outputs.map((x) => x.getCommitment()),
|
||||||
extAmount,
|
publicAmount: BigNumber.from(extAmount).sub(fee).add(FIELD_SIZE).mod(FIELD_SIZE).toString(),
|
||||||
fee,
|
|
||||||
extDataHash,
|
extDataHash,
|
||||||
|
|
||||||
// data for 2 transaction inputs
|
// data for 2 transaction inputs
|
||||||
@ -87,8 +88,7 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
|
|||||||
inputNullifiers: inputs.map((x) => toFixedHex(x.getNullifier())),
|
inputNullifiers: inputs.map((x) => toFixedHex(x.getNullifier())),
|
||||||
outputCommitments: outputs.map((x) => toFixedHex(x.getCommitment())),
|
outputCommitments: outputs.map((x) => toFixedHex(x.getCommitment())),
|
||||||
outPathIndices: toFixedHex(outputIndex >> outputBatchBits),
|
outPathIndices: toFixedHex(outputIndex >> outputBatchBits),
|
||||||
extAmount: toFixedHex(extAmount),
|
publicAmount: toFixedHex(input.publicAmount),
|
||||||
fee: toFixedHex(fee),
|
|
||||||
extDataHash: toFixedHex(extDataHash),
|
extDataHash: toFixedHex(extDataHash),
|
||||||
}
|
}
|
||||||
// console.log('Solidity args', args)
|
// console.log('Solidity args', args)
|
||||||
@ -121,10 +121,7 @@ async function prepareTransaction({
|
|||||||
.add(outputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))
|
.add(outputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))
|
||||||
.sub(inputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))
|
.sub(inputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))
|
||||||
|
|
||||||
const amount = extAmount > 0 ? extAmount : 0 // extAmount will be positive for a deposit, zero for a transact and negative for withdraw
|
const amount = extAmount > 0 ? extAmount : 0
|
||||||
if (extAmount < 0) {
|
|
||||||
extAmount = FIELD_SIZE.add(extAmount)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { args, extData } = await getProof({
|
const { args, extData } = await getProof({
|
||||||
inputs,
|
inputs,
|
||||||
|
26
src/utils.js
26
src/utils.js
@ -14,15 +14,19 @@ const FIELD_SIZE = BigNumber.from(
|
|||||||
/** 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 }) {
|
function getExtDataHash({ recipient, extAmount, relayer, fee, encryptedOutput1, encryptedOutput2 }) {
|
||||||
const abi = new ethers.utils.AbiCoder()
|
const abi = new ethers.utils.AbiCoder()
|
||||||
|
|
||||||
const encodedData = abi.encode(
|
const encodedData = abi.encode(
|
||||||
['tuple(address recipient,address relayer,bytes encryptedOutput1,bytes encryptedOutput2)'],
|
[
|
||||||
|
'tuple(address recipient,int256 extAmount,address relayer,uint256 fee,bytes encryptedOutput1,bytes encryptedOutput2)',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
recipient: toFixedHex(recipient, 20),
|
recipient: toFixedHex(recipient, 20),
|
||||||
|
extAmount: toFixedHex(extAmount),
|
||||||
relayer: toFixedHex(relayer, 20),
|
relayer: toFixedHex(relayer, 20),
|
||||||
|
fee: toFixedHex(fee),
|
||||||
encryptedOutput1: encryptedOutput1,
|
encryptedOutput1: encryptedOutput1,
|
||||||
encryptedOutput2: encryptedOutput2,
|
encryptedOutput2: encryptedOutput2,
|
||||||
},
|
},
|
||||||
@ -33,12 +37,18 @@ function getExtDataHash({ recipient, relayer, encryptedOutput1, encryptedOutput2
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** BigNumber to hex string of specified length */
|
/** BigNumber to hex string of specified length */
|
||||||
const toFixedHex = (number, length = 32) =>
|
function toFixedHex(number, length = 32) {
|
||||||
'0x' +
|
let result =
|
||||||
(number instanceof Buffer
|
'0x' +
|
||||||
? number.toString('hex')
|
(number instanceof Buffer
|
||||||
: BigNumber.from(number).toHexString().slice(2)
|
? number.toString('hex')
|
||||||
).padStart(length * 2, '0')
|
: BigNumber.from(number).toHexString().replace('0x', '')
|
||||||
|
).padStart(length * 2, '0')
|
||||||
|
if (result.indexOf('-') > -1) {
|
||||||
|
result = '-' + result.replace('-', '')
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/** Convert value into buffer of specified byte length */
|
/** Convert value into buffer of specified byte length */
|
||||||
const toBuffer = (value, length) =>
|
const toBuffer = (value, length) =>
|
||||||
|
@ -71,6 +71,14 @@ describe('TornadoPool', () => {
|
|||||||
expect(result).to.be.deep.equal(data)
|
expect(result).to.be.deep.equal(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('constants check', async () => {
|
||||||
|
const maxFee = await tornadoPool.MAX_FEE()
|
||||||
|
const maxExtAmount = await tornadoPool.MAX_EXT_AMOUNT()
|
||||||
|
const fieldSize = await tornadoPool.FIELD_SIZE()
|
||||||
|
|
||||||
|
expect(maxExtAmount.add(maxFee)).to.be.lt(fieldSize)
|
||||||
|
})
|
||||||
|
|
||||||
it('should register and deposit', async function () {
|
it('should register and deposit', async function () {
|
||||||
// Alice deposits into tornado pool
|
// Alice deposits into tornado pool
|
||||||
const aliceDepositAmount = 1e7
|
const aliceDepositAmount = 1e7
|
||||||
|
Loading…
Reference in New Issue
Block a user