This commit is contained in:
Alexey 2019-09-23 21:45:20 +03:00
parent 5ef6e33c78
commit 11f7408def
11 changed files with 3814 additions and 27 deletions

3
.gitignore vendored
View File

@ -96,3 +96,6 @@ typings/
ERC20Mixer_flat.sol
ETHMixer_flat.sol
.openzeppelin/.session
.openzeppelin/dev-*.json

View File

@ -0,0 +1,15 @@
{
"manifestVersion": "2.2",
"contracts": {
"ETHMixer": "ETHMixer"
},
"dependencies": {},
"name": "tornado",
"version": "1.0.0",
"compiler": {
"manager": "truffle",
"compilerSettings": {
"optimizer": {}
}
}
}

View File

@ -31,8 +31,7 @@ template Withdraw(levels, rounds) {
signal input root;
signal input nullifierHash;
signal input receiver; // not taking part in any computations
signal input relayer; // not taking part in any computations
signal input fee; // not taking part in any computations
signal private input nullifier;
signal private input secret;
signal private input pathElements[levels];
@ -56,11 +55,7 @@ template Withdraw(levels, rounds) {
// Most likely it is not required, but it's better to stay on the safe side and it only takes 2 constraints
// Squares are used to prevent optimizer from removing those constraints
signal receiverSquare;
signal feeSquare;
signal relayerSquare;
receiverSquare <== receiver * receiver;
feeSquare <== fee * fee;
relayerSquare <== relayer * relayer;
}
component main = Withdraw(16, 220);

98
cli.js
View File

@ -11,6 +11,8 @@ const merkleTree = require('./lib/MerkleTree')
const Web3 = require('web3')
const buildGroth16 = require('websnark/src/groth16')
const websnarkUtils = require('websnark/src/utils')
const { GSNProvider, GSNDevProvider } = require('@openzeppelin/gsn-provider')
const { ephemeral } = require('@openzeppelin/network')
let web3, mixer, erc20mixer, circuit, proving_key, groth16, erc20
let MERKLE_TREE_HEIGHT, ETH_AMOUNT, EMPTY_ELEMENT, ERC20_TOKEN
@ -194,11 +196,83 @@ async function withdraw(note, receiver) {
console.log('Done')
}
async function withdrawViaRelayer(note, receiver) {
// Decode hex string and restore the deposit object
let buf = Buffer.from(note.slice(2), 'hex')
let deposit = createDeposit(bigInt.leBuff2int(buf.slice(0, 31)), bigInt.leBuff2int(buf.slice(31, 62)))
const nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31))
const paddedNullifierHash = nullifierHash.toString(16).padStart('66', '0x000000')
const paddedCommitment = deposit.commitment.toString(16).padStart('66', '0x000000')
// Get all deposit events from smart contract and assemble merkle tree from them
console.log('Getting current state from mixer contract')
const events = await mixer.getPastEvents('Deposit', { fromBlock: mixer.deployedBlock, toBlock: 'latest' })
const leaves = events
.sort((a, b) => a.returnValues.leafIndex.sub(b.returnValues.leafIndex)) // Sort events in chronological order
.map(e => e.returnValues.commitment)
const tree = new merkleTree(MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, leaves)
// Find current commitment in the tree
let depositEvent = events.find(e => e.returnValues.commitment.eq(paddedCommitment))
let leafIndex = depositEvent ? depositEvent.returnValues.leafIndex.toNumber() : -1
// Validate that our data is correct
const isValidRoot = await mixer.methods.isKnownRoot(await tree.root()).call()
const isSpent = await mixer.methods.isSpent(paddedNullifierHash).call()
assert(isValidRoot === true, 'Merkle tree assembled incorrectly') // Merkle tree assembled correctly
assert(isSpent === false, 'The note is spent') // The note is not spent
assert(leafIndex >= 0, 'Our deposit is not present in the tree') // Our deposit is present in the tree
// Compute merkle proof of our commitment
const { root, path_elements, path_index } = await tree.path(leafIndex)
// Prepare circuit input
const input = {
// Public snark inputs
root: root,
nullifierHash,
receiver: bigInt(receiver),
// Private snark inputs
nullifier: deposit.nullifier,
secret: deposit.secret,
pathElements: path_elements,
pathIndex: path_index,
}
console.log('Generating SNARK proof')
console.time('Proof time')
const proof = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key)
const { pi_a, pi_b, pi_c, publicSignals } = websnarkUtils.toSolidityInput(proof)
console.timeEnd('Proof time')
console.log('Submitting withdraw transaction via relayer')
const account = ephemeral()
const HARDCODED_RELAYER_OPTS = {
txFee: 90,
fixedGasPrice: 22000000001,
gasPrice: 22000000001,
fixedGasLimit: 5000000,
gasLimit: 5000000,
verbose: true,
}
// const provider = new GSNProvider('https://rinkeby.infura.io/v3/c7463beadf2144e68646ff049917b716', { signKey: account })
const provider = new GSNDevProvider('http://localhost:8545', { signKey: account, ...HARDCODED_RELAYER_OPTS })
web3 = new Web3(provider)
const netId = await web3.eth.net.getId()
// eslint-disable-next-line require-atomic-updates
mixer = new web3.eth.Contract(contractJson.abi, contractJson.networks[netId].address)
console.log('mixer address', contractJson.networks[netId].address)
const tx = await mixer.methods.withdrawViaRelayer(pi_a, pi_b, pi_c, publicSignals).send({ from: account.address, gas: '5000000' })
console.log('tx', tx)
console.log('Done')
}
/**
* Init web3, contracts, and snark
*/
let contractJson, erc20ContractJson, erc20mixerJson
async function init() {
let contractJson, erc20ContractJson, erc20mixerJson
if (inBrowser) {
// Initialize using injected web3 (Metamask)
// To assemble web version run `npm run browserify`
@ -207,7 +281,7 @@ async function init() {
circuit = await (await fetch('build/circuits/withdraw.json')).json()
proving_key = await (await fetch('build/circuits/withdraw_proving_key.bin')).arrayBuffer()
MERKLE_TREE_HEIGHT = 16
ETH_AMOUNT = 1e18
ETH_AMOUNT = '30000000000000000'
EMPTY_ELEMENT = 1
} else {
// Initialize from local node
@ -231,9 +305,11 @@ async function init() {
mixer.deployedBlock = tx.blockNumber
}
const tx3 = await web3.eth.getTransaction(erc20mixerJson.networks[netId].transactionHash)
erc20mixer = new web3.eth.Contract(erc20mixerJson.abi, erc20mixerJson.networks[netId].address)
erc20mixer.deployedBlock = tx3.blockNumber
if (erc20mixerJson) {
const tx3 = await web3.eth.getTransaction(erc20mixerJson.networks[netId].transactionHash)
erc20mixer = new web3.eth.Contract(erc20mixerJson.abi, erc20mixerJson.networks[netId].address)
erc20mixer.deployedBlock = tx3.blockNumber
}
if(ERC20_TOKEN === '') {
erc20 = new web3.eth.Contract(erc20ContractJson.abi, erc20ContractJson.networks[netId].address)
@ -273,6 +349,11 @@ if (inBrowser) {
const receiver = (await web3.eth.getAccounts())[0]
await withdraw(note, receiver)
}
window.withdrawViaRelayer = async () => {
const note = prompt('Enter the note to withdrawViaRelayer')
const receiver = (await web3.eth.getAccounts())[0]
await withdrawViaRelayer(note, receiver)
}
init()
} else {
const args = process.argv.slice(2)
@ -320,6 +401,13 @@ if (inBrowser) {
else
printHelp(1)
break
case 'withdrawViaRelayer':
if (args.length === 3 && /^0x[0-9a-fA-F]{124}$/.test(args[1]) && /^0x[0-9a-fA-F]{40}$/.test(args[2])) {
init().then(() => withdrawViaRelayer(args[1], args[2])).then(() => process.exit(0)).catch(err => {console.log(err); process.exit(1)})
}
else
printHelp(1)
break
case 'auto':
if (args.length === 1) {
(async () => {

View File

@ -12,8 +12,10 @@
pragma solidity ^0.5.8;
import "./Mixer.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/GSN/GSNRecipient.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/GSN/IRelayHub.sol";
contract ETHMixer is Mixer {
contract ETHMixer is Mixer, GSNRecipient {
constructor(
address _verifier,
uint256 _mixDenomination,
@ -23,14 +25,77 @@ contract ETHMixer is Mixer {
) Mixer(_verifier, _mixDenomination, _merkleTreeHeight, _emptyElement, _operator) public {
}
function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal {
_receiver.transfer(mixDenomination - _fee);
if (_fee > 0) {
_relayer.transfer(_fee);
}
function _processWithdraw(address payable _receiver) internal {
_receiver.transfer(mixDenomination);
}
function _processDeposit() internal {
require(msg.value == mixDenomination, "Please send `mixDenomination` ETH along with transaction");
}
function withdrawViaRelayer(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[3] memory input) public {
uint256 root = input[0];
uint256 nullifierHash = input[1];
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(a, b, c, input), "Invalid withdraw proof");
nullifierHashes[nullifierHash] = true;
// we will process withdraw in postRelayedCall func
}
// gsn related stuff
// this func is called by a Relayer via the RelayerHub before sending a tx
function acceptRelayedCall(
address relay,
address from,
bytes calldata encodedFunction,
uint256 transactionFee,
uint256 gasPrice,
uint256 gasLimit,
uint256 nonce,
bytes calldata approvalData,
uint256 maxPossibleCharge
) external view returns (uint256, bytes memory) {
// think of a withdraw dry-run
if (_computeCharge(gasLimit, gasPrice, transactionFee) * 2 > mixDenomination) {
return (1, "Fee exceeds 50% of transfer value");
}
if (!compareBytesWithSelector(encodedFunction, this.withdrawViaRelayer.selector)) {
return (2, "Only withdrawViaRelayer can be called");
}
return _approveRelayedCall();
}
// this func is called by RelayerHub right before calling a target func
function preRelayedCall(bytes calldata /*context*/) external returns (bytes32) {}
event Debug(uint actualCharge, bytes context, address recipient);
// this func is called by RelayerHub right after calling a target func
function postRelayedCall(bytes memory context, bool /*success*/, uint actualCharge, bytes32 /*preRetVal*/) public {
IRelayHub relayHub = IRelayHub(getHubAddr());
address payable recipient;
assembly {
recipient := sload(add(context, 324)) // 4 + (8 * 32) + (32) + (32) == selector + proof + root + nullifier
}
emit Debug(actualCharge, context, recipient);
recipient.transfer(mixDenomination - actualCharge);
relayHub.depositFor.value(actualCharge)(address(this));
// or we can send actualCharge somewhere else...
}
function compareBytesWithSelector(bytes memory data, bytes4 sel) internal pure returns (bool) {
return data[0] == sel[0]
&& data[1] == sel[1]
&& data[2] == sel[2]
&& data[3] == sel[3];
}
function withdrawFundsFromHub(uint256 amount, address payable dest) external {
require(msg.sender == operator, "unauthorized");
IRelayHub(getHubAddr()).withdraw(amount, dest);
}
}

View File

@ -14,7 +14,7 @@ pragma solidity ^0.5.8;
import "./MerkleTreeWithHistory.sol";
contract IVerifier {
function verifyProof(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[5] memory input) public returns(bool);
function verifyProof(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[3] memory input) public returns(bool);
}
contract Mixer is MerkleTreeWithHistory {
@ -29,7 +29,7 @@ contract Mixer is MerkleTreeWithHistory {
uint256 public mixDenomination;
event Deposit(uint256 indexed commitment, uint256 leafIndex, uint256 timestamp);
event Withdraw(address to, uint256 nullifierHash, address indexed relayer, uint256 fee);
event Withdraw(address to, uint256 nullifierHash, address indexed relayer);
/**
@dev The constructor
@ -75,20 +75,17 @@ contract Mixer is MerkleTreeWithHistory {
- the receiver of funds
- optional fee that goes to the transaction sender (usually a relay)
*/
function withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[5] memory input) public {
function withdraw(uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, uint256[3] memory input) public {
uint256 root = input[0];
uint256 nullifierHash = input[1];
address payable receiver = address(input[2]);
address payable relayer = address(input[3]);
uint256 fee = input[4];
require(fee < mixDenomination, "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(a, b, c, input), "Invalid withdraw proof");
nullifierHashes[nullifierHash] = true;
_processWithdraw(receiver, relayer, fee);
emit Withdraw(receiver, nullifierHash, relayer, fee);
_processWithdraw(receiver);
emit Withdraw(receiver, nullifierHash, receiver);
}
function toggleDeposits() external {
@ -106,6 +103,5 @@ contract Mixer is MerkleTreeWithHistory {
}
function _processDeposit() internal {}
function _processWithdraw(address payable _receiver, address payable _relayer, uint256 _fee) internal {}
function _processWithdraw(address payable _receiver) internal {}
}

View File

@ -10,6 +10,7 @@
Make sure your Metamask is unlocked and connected to Kovan (or other network you've deployed your contract to)<br>
<a href="#" onclick="deposit()">Deposit</a>
<a href="#" onclick="withdraw()">Withdraw</a>
<a href="#" onclick="withdrawViaRelayer()">withdrawViaRelayer</a>
</p>
<script src="index.js"></script>
</body>

View File

@ -13,5 +13,6 @@ module.exports = function(deployer, network, accounts) {
await ETHMixer.link(MiMC, miMC.address)
const mixer = await deployer.deploy(ETHMixer, verifier.address, ETH_AMOUNT, MERKLE_TREE_HEIGHT, EMPTY_ELEMENT, accounts[0])
console.log('ETHMixer\'s address ', mixer.address)
const tx = await mixer.initialize()
})
}

3609
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,11 @@
"websnark": "git+https://github.com/peppersec/websnark.git#ed6a4d8a6fb081a62af26820980046bbb602d559"
},
"devDependencies": {
"@openzeppelin/contracts-ethereum-package": "^2.2.3",
"@openzeppelin/gsn-helpers": "^0.2.0",
"@openzeppelin/gsn-provider": "^0.1.7",
"@openzeppelin/network": "^0.2.9",
"@openzeppelin/upgrades": "^2.5.3",
"truffle-flattener": "^1.4.0"
}
}

View File

@ -51,6 +51,15 @@ module.exports = {
// timeoutBlocks: 200,
skipDryRun: true
},
rinkeby: {
provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'https://rinkeby.infura.io/v3/c7463beadf2144e68646ff049917b716'),
network_id: 4,
gas: 6000000,
gasPrice: utils.toWei('1', 'gwei'),
// confirmations: 0,
// timeoutBlocks: 200,
skipDryRun: true
},
mainnet: {
provider: () => new HDWalletProvider(process.env.PRIVATE_KEY, 'http://ethereum-rpc.trustwalletapp.com'),
network_id: 1,