mirror of
https://github.com/tornadocash/tornado-nova
synced 2024-02-02 14:53:56 +01:00
utxo class, separate private key for each input
This commit is contained in:
parent
238431233b
commit
bd2252afa3
@ -26,11 +26,10 @@ template Transaction(levels, zeroLeaf) {
|
|||||||
signal input fee;
|
signal input fee;
|
||||||
signal input extDataHash;
|
signal input extDataHash;
|
||||||
|
|
||||||
signal private input privateKey;
|
|
||||||
|
|
||||||
// data for 2 transaction inputs
|
// data for 2 transaction inputs
|
||||||
signal private input inAmount[2];
|
signal private input inAmount[2];
|
||||||
signal private input inBlinding[2];
|
signal private input inBlinding[2];
|
||||||
|
signal private input inPrivateKey[2];
|
||||||
signal private input inPathIndices[2];
|
signal private input inPathIndices[2];
|
||||||
signal private input inPathElements[2][levels];
|
signal private input inPathElements[2][levels];
|
||||||
|
|
||||||
@ -42,6 +41,7 @@ template Transaction(levels, zeroLeaf) {
|
|||||||
signal private input outPathElements[levels - 1];
|
signal private input outPathElements[levels - 1];
|
||||||
|
|
||||||
component inUtxoHasher[2];
|
component inUtxoHasher[2];
|
||||||
|
component inKeypair[2];
|
||||||
component outUtxoHasher[2];
|
component outUtxoHasher[2];
|
||||||
component nullifierHasher[2];
|
component nullifierHasher[2];
|
||||||
component checkRoot[2]
|
component checkRoot[2]
|
||||||
@ -49,20 +49,20 @@ template Transaction(levels, zeroLeaf) {
|
|||||||
component inAmountCheck[2];
|
component inAmountCheck[2];
|
||||||
component outAmountCheck[2];
|
component outAmountCheck[2];
|
||||||
|
|
||||||
component keypair = Keypair();
|
|
||||||
keypair.privateKey <== privateKey;
|
|
||||||
|
|
||||||
// verify correctness of transaction inputs
|
// verify correctness of transaction inputs
|
||||||
for (var tx = 0; tx < 2; tx++) {
|
for (var tx = 0; tx < 2; tx++) {
|
||||||
|
inKeypair[tx] = Keypair();
|
||||||
|
inKeypair[tx].privateKey <== inPrivateKey[tx];
|
||||||
|
|
||||||
inUtxoHasher[tx] = TransactionHasher();
|
inUtxoHasher[tx] = TransactionHasher();
|
||||||
inUtxoHasher[tx].amount <== inAmount[tx];
|
inUtxoHasher[tx].amount <== inAmount[tx];
|
||||||
inUtxoHasher[tx].blinding <== inBlinding[tx];
|
inUtxoHasher[tx].blinding <== inBlinding[tx];
|
||||||
inUtxoHasher[tx].publicKey <== keypair.publicKey;
|
inUtxoHasher[tx].publicKey <== inKeypair[tx].publicKey;
|
||||||
|
|
||||||
nullifierHasher[tx] = NullifierHasher();
|
nullifierHasher[tx] = NullifierHasher();
|
||||||
nullifierHasher[tx].commitment <== inUtxoHasher[tx].commitment;
|
nullifierHasher[tx].commitment <== inUtxoHasher[tx].commitment;
|
||||||
nullifierHasher[tx].merklePath <== inPathIndices[tx];
|
nullifierHasher[tx].merklePath <== inPathIndices[tx];
|
||||||
nullifierHasher[tx].privateKey <== keypair.privateKey;
|
nullifierHasher[tx].privateKey <== inPrivateKey[tx];
|
||||||
nullifierHasher[tx].nullifier === inputNullifier[tx];
|
nullifierHasher[tx].nullifier === inputNullifier[tx];
|
||||||
|
|
||||||
tree[tx] = MerkleTree(levels);
|
tree[tx] = MerkleTree(levels);
|
||||||
|
@ -29,8 +29,8 @@ contract TornadoPool is ReentrancyGuard {
|
|||||||
IVerifier public verifier;
|
IVerifier public verifier;
|
||||||
|
|
||||||
struct ExtData {
|
struct ExtData {
|
||||||
address payable _recipient;
|
address payable recipient;
|
||||||
address payable _relayer;
|
address payable relayer;
|
||||||
bytes encryptedOutput1;
|
bytes encryptedOutput1;
|
||||||
bytes encryptedOutput2;
|
bytes encryptedOutput2;
|
||||||
}
|
}
|
||||||
@ -87,11 +87,11 @@ 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");
|
||||||
_extData._recipient.transfer(uint256(-extAmount));
|
_extData.recipient.transfer(uint256(-extAmount));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_fee > 0) {
|
if (_fee > 0) {
|
||||||
_extData._relayer.transfer(_fee);
|
_extData.relayer.transfer(_fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo enforce currentCommitmentIndex value in snark
|
// todo enforce currentCommitmentIndex value in snark
|
||||||
|
330
src/index.js
330
src/index.js
@ -3,127 +3,88 @@ const MerkleTree = require('fixed-merkle-tree')
|
|||||||
const Web3 = require('web3')
|
const Web3 = require('web3')
|
||||||
const { ethers } = require('hardhat')
|
const { ethers } = require('hardhat')
|
||||||
const { BigNumber } = ethers
|
const { BigNumber } = ethers
|
||||||
const { randomBN, bitsToNumber, toFixedHex, toBuffer, poseidonHash, poseidonHash2 } = require('./utils')
|
const { toFixedHex, poseidonHash2, getExtDataHash, FIELD_SIZE } = require('./utils')
|
||||||
|
const Utxo = require('./utxo')
|
||||||
|
|
||||||
let contract, web3
|
let contract, web3
|
||||||
const { prove } = require('./prover')
|
const { prove } = require('./prover')
|
||||||
const FIELD_SIZE = '21888242871839275222246405745257275088548364400416034343698204186575808495617'
|
|
||||||
const MERKLE_TREE_HEIGHT = 5
|
const MERKLE_TREE_HEIGHT = 5
|
||||||
const RPC_URL = 'http://localhost:8545'
|
const RPC_URL = 'http://localhost:8545'
|
||||||
|
|
||||||
function fromPrivkey(privkey) {
|
|
||||||
return {
|
|
||||||
privkey,
|
|
||||||
pubkey: poseidonHash([privkey]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomKeypair() {
|
|
||||||
return fromPrivkey(randomBN())
|
|
||||||
}
|
|
||||||
|
|
||||||
function createZeroUtxo(keypair) {
|
|
||||||
return createUtxo(
|
|
||||||
0,
|
|
||||||
randomBN(),
|
|
||||||
keypair.pubkey,
|
|
||||||
keypair.privkey,
|
|
||||||
Array(MERKLE_TREE_HEIGHT).fill(0),
|
|
||||||
Array(MERKLE_TREE_HEIGHT).fill(0),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOutput(amount, pubkey) {
|
|
||||||
if (!pubkey) {
|
|
||||||
throw new Error('no pubkey')
|
|
||||||
}
|
|
||||||
return createUtxo(amount, randomBN(), pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
function createInput({ amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements }) {
|
|
||||||
return createUtxo(amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// unsafe function without sanity checks
|
|
||||||
function createUtxo(amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements) {
|
|
||||||
let utxo = { amount, blinding, pubkey, privkey, merklePathIndices, merklePathElements }
|
|
||||||
utxo.commitment = poseidonHash([amount, blinding, pubkey])
|
|
||||||
if (privkey) {
|
|
||||||
utxo.nullifier = poseidonHash([utxo.commitment, bitsToNumber(merklePathIndices), privkey])
|
|
||||||
}
|
|
||||||
return utxo
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDeposit(amount, keypair) {
|
|
||||||
const fakeKeypair = randomKeypair()
|
|
||||||
const output = createOutput(amount, keypair.pubkey)
|
|
||||||
output.privkey = keypair.privkey
|
|
||||||
const tx = {
|
|
||||||
inputs: [createZeroUtxo(fakeKeypair), createZeroUtxo(fakeKeypair)],
|
|
||||||
outputs: [output, createZeroUtxo(fakeKeypair)], // todo shuffle
|
|
||||||
}
|
|
||||||
return tx
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildMerkleTree() {
|
async function buildMerkleTree() {
|
||||||
console.log('Getting contract state...')
|
console.log('Getting contract state...')
|
||||||
const events = await contract.getPastEvents('NewCommitment', { fromBlock: 0, toBlock: 'latest' })
|
const events = await contract.getPastEvents('NewCommitment', { fromBlock: 0, toBlock: 'latest' })
|
||||||
const leaves = events
|
const leaves = events
|
||||||
.sort((a, b) => a.returnValues.index - b.returnValues.index) // todo sort by event date
|
.sort((a, b) => a.returnValues.index - b.returnValues.index) // todo sort by event date
|
||||||
.map((e) => toFixedHex(e.returnValues.commitment))
|
.map((e) => toFixedHex(e.returnValues.commitment))
|
||||||
console.log('leaves', leaves)
|
// console.log('leaves', leaves)
|
||||||
return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 })
|
return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertOutput(tree, output) {
|
async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, relayer }) {
|
||||||
await tree.insert(output.commitment)
|
// todo shuffle inputs and outputs
|
||||||
let { pathElements, pathIndices } = await tree.path(tree.elements().length - 1)
|
if (inputs.length !== 2 || outputs.length !== 2 ) {
|
||||||
output.merklePathIndices = pathIndices
|
throw new Error('Unsupported number of inputs/outputs')
|
||||||
output.merklePathElements = pathElements
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getProof({ input1, input2, output1, output2, tree, extAmount, fee, recipient, relayer }) {
|
|
||||||
const oldRoot = tree.root()
|
|
||||||
|
|
||||||
// if deposit require(extAmount > 0)
|
|
||||||
|
|
||||||
if (input1.amount !== 0) {
|
|
||||||
// transact
|
|
||||||
const index1 = await tree.indexOf(toFixedHex(input1.commitment))
|
|
||||||
const path1 = await tree.path(index1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await insertOutput(tree, output1)
|
let inputMerklePathIndices = []
|
||||||
await insertOutput(tree, output2)
|
let inputMerklePathElements = []
|
||||||
|
|
||||||
extData = recipient + relayer // TODO
|
for (const input of inputs) {
|
||||||
|
if (input.amount > 0) {
|
||||||
|
const index = tree.indexOf(toFixedHex(input.getCommitment()))
|
||||||
|
if (index < 0) {
|
||||||
|
throw new Error(`Input commitment ${input.getCommitment()} was not found`)
|
||||||
|
}
|
||||||
|
inputMerklePathIndices.push(index)
|
||||||
|
inputMerklePathElements.push(tree.path(index).pathElements)
|
||||||
|
} else {
|
||||||
|
inputMerklePathIndices.push(0)
|
||||||
|
inputMerklePathElements.push(new Array(tree.levels).fill(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldRoot = tree.root()
|
||||||
|
for (const output of outputs) {
|
||||||
|
output.index = tree.elements().length
|
||||||
|
tree.insert(output.getCommitment())
|
||||||
|
}
|
||||||
|
const outputIndex = tree.elements().length - 1
|
||||||
|
const outputPath = tree.path(outputIndex).pathElements.slice(1)
|
||||||
|
|
||||||
|
const extData = {
|
||||||
|
recipient: toFixedHex(recipient, 20),
|
||||||
|
relayer: toFixedHex(relayer, 20),
|
||||||
|
encryptedOutput1: '0xff',
|
||||||
|
encryptedOutput2: '0xff',
|
||||||
|
}
|
||||||
|
|
||||||
|
const extDataHash = getExtDataHash(extData)
|
||||||
let input = {
|
let input = {
|
||||||
root: oldRoot,
|
root: oldRoot,
|
||||||
newRoot: tree.root(),
|
newRoot: tree.root(),
|
||||||
inputNullifier: [input1.nullifier, input2.nullifier],
|
inputNullifier: inputs.map(x => x.getNullifier()),
|
||||||
outputCommitment: [outputs1.commitment, outputs2.commitment],
|
outputCommitment: outputs.map(x => x.getCommitment()),
|
||||||
extAmount,
|
extAmount,
|
||||||
fee,
|
fee,
|
||||||
extData,
|
extDataHash,
|
||||||
|
|
||||||
// private inputs
|
|
||||||
privateKey: inputs1.privkey, // TODO make sure you use the right one when you shuffle inputs
|
|
||||||
|
|
||||||
// data for 2 transaction inputs
|
// data for 2 transaction inputs
|
||||||
inAmount: [inputs1.amount, inputs2.amount],
|
inAmount: inputs.map(x => x.amount),
|
||||||
inBlinding: [inputs1.blinding, inputs2.blinding],
|
inPrivateKey: inputs.map(x => x.privkey),
|
||||||
inPathIndices: [bitsToNumber(inputs1.merklePathIndices), bitsToNumber(inputs2.merklePathIndices)],
|
inBlinding: inputs.map(x => x.blinding),
|
||||||
inPathElements: [inputs1.merklePathElements, inputs2.merklePathElements],
|
inPathIndices: inputMerklePathIndices,
|
||||||
|
inPathElements: inputMerklePathElements,
|
||||||
|
|
||||||
// data for 2 transaction outputs
|
// data for 2 transaction outputs
|
||||||
outAmount: [outputs1.amount, outputs2.amount],
|
outAmount: outputs.map(x => x.amount),
|
||||||
outBlinding: [outputs1.blinding, outputs2.blinding],
|
outBlinding: outputs.map(x => x.blinding),
|
||||||
outPubkey: [outputs1.pubkey, outputs2.pubkey],
|
outPubkey: outputs.map(x => x.pubkey),
|
||||||
outPathIndices: bitsToNumber[outputs1.merklePathIndices.slice(1)],
|
outPathIndices: outputIndex >> 1,
|
||||||
outPathElements: [outputs1.merklePathElements.slice(1)],
|
outPathElements: outputPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('SNARK input', input)
|
//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')
|
||||||
@ -131,12 +92,14 @@ async function getProof({ input1, input2, output1, output2, tree, extAmount, fee
|
|||||||
const args = [
|
const args = [
|
||||||
toFixedHex(input.root),
|
toFixedHex(input.root),
|
||||||
toFixedHex(input.newRoot),
|
toFixedHex(input.newRoot),
|
||||||
[toFixedHex(input1.nullifier), toFixedHex(input2.nullifier)],
|
inputs.map(x => toFixedHex(x.getNullifier())),
|
||||||
[toFixedHex(output1.commitment), toFixedHex(output2.commitment)],
|
outputs.map(x => toFixedHex(x.getCommitment())),
|
||||||
toFixedHex(0),
|
toFixedHex(extAmount),
|
||||||
toFixedHex(input.fee),
|
toFixedHex(fee),
|
||||||
toFixedHex(extData), // extData hash actually
|
extData,
|
||||||
|
toFixedHex(extDataHash),
|
||||||
]
|
]
|
||||||
|
// console.log('Solidity args', args)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
proof,
|
proof,
|
||||||
@ -146,16 +109,13 @@ async function getProof({ input1, input2, output1, output2, tree, extAmount, fee
|
|||||||
|
|
||||||
async function deposit() {
|
async function deposit() {
|
||||||
const amount = 1e6
|
const amount = 1e6
|
||||||
const tree = await buildMerkleTree()
|
const inputs = [new Utxo(), new Utxo()]
|
||||||
const keypair = randomKeypair()
|
const outputs = [new Utxo({ amount }), new Utxo()]
|
||||||
const tx = createDeposit(amount, keypair)
|
|
||||||
|
|
||||||
const { proof, args } = await getProof({
|
const { proof, args } = await getProof({
|
||||||
input1: tx.inputs[0],
|
inputs,
|
||||||
input2: tx.inputs[1],
|
outputs,
|
||||||
output1: tx.outputs[1],
|
tree: await buildMerkleTree(),
|
||||||
output2: tx.outputs[1],
|
|
||||||
tree,
|
|
||||||
extAmount: amount,
|
extAmount: amount,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
recipient: 0,
|
recipient: 0,
|
||||||
@ -167,155 +127,47 @@ async function deposit() {
|
|||||||
.transaction(proof, ...args)
|
.transaction(proof, ...args)
|
||||||
.send({ value: amount, from: web3.eth.defaultAccount, gas: 1e6 })
|
.send({ value: amount, from: web3.eth.defaultAccount, gas: 1e6 })
|
||||||
console.log(`Receipt ${receipt.transactionHash}`)
|
console.log(`Receipt ${receipt.transactionHash}`)
|
||||||
return tx.outputs[0]
|
return outputs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function transact(txOutput) {
|
async function transact(utxo) {
|
||||||
console.log('txOutput', txOutput)
|
const inputs = [utxo, new Utxo()]
|
||||||
const tree = await buildMerkleTree()
|
const outputs = [
|
||||||
console.log('tree', tree)
|
new Utxo({ amount: utxo.amount / 4 }),
|
||||||
const oldRoot = await tree.root()
|
new Utxo({ amount: utxo.amount * 3 / 4, privkey: utxo.privkey}),
|
||||||
const keypair = randomKeypair()
|
]
|
||||||
|
|
||||||
const index = await tree.indexOf(toFixedHex(txOutput.commitment))
|
const { proof, args } = await getProof({
|
||||||
console.log('index', index)
|
inputs,
|
||||||
const { pathElements, pathIndices } = await tree.path(index)
|
outputs,
|
||||||
console.log('pathIndices', pathIndices)
|
tree: await buildMerkleTree(),
|
||||||
txOutput.merklePathElements = pathElements
|
|
||||||
const input1 = createInput(txOutput)
|
|
||||||
const tx = {
|
|
||||||
inputs: [input1, createZeroUtxo(fromPrivkey(txOutput.privkey))],
|
|
||||||
outputs: [
|
|
||||||
createOutput(txOutput.amount / 4, keypair.pubkey),
|
|
||||||
createOutput((txOutput.amount * 3) / 4, txOutput.pubkey),
|
|
||||||
], // todo shuffle
|
|
||||||
}
|
|
||||||
tx.outputs[0].privkey = keypair.privkey
|
|
||||||
tx.outputs[1].privkey = txOutput.privkey
|
|
||||||
await insertOutput(tree, tx.outputs[0])
|
|
||||||
await insertOutput(tree, tx.outputs[1])
|
|
||||||
console.log('Note', tx.outputs[0])
|
|
||||||
|
|
||||||
let input = {
|
|
||||||
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: 0,
|
extAmount: 0,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
recipient: 0,
|
recipient: 0,
|
||||||
relayer: 0,
|
relayer: 0,
|
||||||
|
})
|
||||||
// private inputs
|
|
||||||
privateKey: tx.inputs[0].privkey,
|
|
||||||
|
|
||||||
// data for 2 transaction inputs
|
|
||||||
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('TRANSFER input', input)
|
|
||||||
|
|
||||||
console.log('Generating SNARK proof...')
|
|
||||||
const proof = await prove(input, './artifacts/circuits/transaction')
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
toFixedHex(input.root),
|
|
||||||
toFixedHex(input.newRoot),
|
|
||||||
[toFixedHex(tx.inputs[0].nullifier), toFixedHex(tx.inputs[1].nullifier)],
|
|
||||||
[toFixedHex(tx.outputs[0].commitment), toFixedHex(tx.outputs[1].commitment)],
|
|
||||||
toFixedHex(0),
|
|
||||||
toFixedHex(input.fee),
|
|
||||||
toFixedHex(input.recipient, 20),
|
|
||||||
toFixedHex(input.relayer, 20),
|
|
||||||
]
|
|
||||||
|
|
||||||
console.log('Sending transfer transaction...')
|
console.log('Sending transfer transaction...')
|
||||||
const receipt = await contract.methods
|
const receipt = await contract.methods
|
||||||
.transaction(proof, ...args)
|
.transaction(proof, ...args)
|
||||||
.send({ from: web3.eth.defaultAccount, gas: 1e6 })
|
.send({ from: web3.eth.defaultAccount, gas: 1e6 })
|
||||||
console.log(`Receipt ${receipt.transactionHash}`)
|
console.log(`Receipt ${receipt.transactionHash}`)
|
||||||
return tx.outputs[0]
|
return outputs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withdraw(txOutput) {
|
async function withdraw(utxo) {
|
||||||
console.log('txOutput', txOutput)
|
const inputs = [utxo, new Utxo()]
|
||||||
const tree = await buildMerkleTree()
|
const outputs = [new Utxo(), new Utxo()]
|
||||||
const oldRoot = await tree.root()
|
|
||||||
|
|
||||||
const index = await tree.indexOf(toFixedHex(txOutput.commitment))
|
const { proof, args } = await getProof({
|
||||||
console.log('index', index)
|
inputs,
|
||||||
const { pathElements, pathIndices } = await tree.path(index)
|
outputs,
|
||||||
console.log('pathIndices', pathIndices)
|
tree: await buildMerkleTree(),
|
||||||
txOutput.merklePathElements = pathElements
|
extAmount: FIELD_SIZE.sub(utxo.amount),
|
||||||
const input1 = createInput(txOutput)
|
|
||||||
const fakeKeypair = randomKeypair()
|
|
||||||
const tx = {
|
|
||||||
inputs: [input1, createZeroUtxo(fromPrivkey(txOutput.privkey))],
|
|
||||||
outputs: [createZeroUtxo(fakeKeypair), createZeroUtxo(fakeKeypair)], // todo shuffle
|
|
||||||
}
|
|
||||||
await insertOutput(tree, tx.outputs[0])
|
|
||||||
await insertOutput(tree, tx.outputs[1])
|
|
||||||
|
|
||||||
let input = {
|
|
||||||
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: BigNumber.from(FIELD_SIZE).sub(BigNumber.from(txOutput.amount)),
|
|
||||||
fee: 0,
|
fee: 0,
|
||||||
recipient: '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48',
|
recipient: '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48',
|
||||||
relayer: 0,
|
relayer: 0,
|
||||||
|
})
|
||||||
// private inputs
|
|
||||||
privateKey: tx.inputs[0].privkey,
|
|
||||||
|
|
||||||
// data for 2 transaction inputs
|
|
||||||
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('WITHDRAW input', input)
|
|
||||||
|
|
||||||
console.log('Generating SNARK proof...')
|
|
||||||
const proof = await prove(input, './artifacts/circuits/transaction')
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
toFixedHex(input.root),
|
|
||||||
toFixedHex(input.newRoot),
|
|
||||||
[toFixedHex(tx.inputs[0].nullifier), toFixedHex(tx.inputs[1].nullifier)],
|
|
||||||
[toFixedHex(tx.outputs[0].commitment), toFixedHex(tx.outputs[1].commitment)],
|
|
||||||
toFixedHex(input.extAmount),
|
|
||||||
toFixedHex(input.fee),
|
|
||||||
toFixedHex(input.recipient, 20),
|
|
||||||
toFixedHex(input.relayer, 20),
|
|
||||||
]
|
|
||||||
|
|
||||||
console.log('args', args)
|
|
||||||
|
|
||||||
console.log('Sending withdraw transaction...')
|
console.log('Sending withdraw transaction...')
|
||||||
const receipt = await contract.methods
|
const receipt = await contract.methods
|
||||||
@ -333,12 +185,12 @@ async function main() {
|
|||||||
})
|
})
|
||||||
netId = await web3.eth.net.getId()
|
netId = await web3.eth.net.getId()
|
||||||
const contractData = require('../artifacts/contracts/TornadoPool.sol/TornadoPool.json')
|
const contractData = require('../artifacts/contracts/TornadoPool.sol/TornadoPool.json')
|
||||||
contract = new web3.eth.Contract(contractData.abi, '0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9')
|
contract = new web3.eth.Contract(contractData.abi, '0x0E801D84Fa97b50751Dbf25036d067dCf18858bF')
|
||||||
web3.eth.defaultAccount = (await web3.eth.getAccounts())[0]
|
web3.eth.defaultAccount = (await web3.eth.getAccounts())[0]
|
||||||
|
|
||||||
const txOutput = await deposit()
|
const utxo1 = await deposit()
|
||||||
const txOutput1 = await transact(txOutput)
|
const utxo2 = await transact(utxo1)
|
||||||
await withdraw(txOutput1)
|
await withdraw(utxo2)
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
@ -8,7 +8,7 @@ const exec = util.promisify(require('child_process').exec)
|
|||||||
|
|
||||||
function prove(input, keyBasePath) {
|
function prove(input, keyBasePath) {
|
||||||
input = utils.stringifyBigInts(input)
|
input = utils.stringifyBigInts(input)
|
||||||
console.log('input', input)
|
// console.log('input', input)
|
||||||
return tmp.dir().then(async (dir) => {
|
return tmp.dir().then(async (dir) => {
|
||||||
dir = dir.path
|
dir = dir.path
|
||||||
let out
|
let out
|
||||||
@ -28,6 +28,8 @@ function prove(input, keyBasePath) {
|
|||||||
out = await exec(
|
out = await exec(
|
||||||
`zkutil prove -c ${keyBasePath}.r1cs -p ${keyBasePath}.params -w ${dir}/witness.json -r ${dir}/proof.json -o ${dir}/public.json`,
|
`zkutil prove -c ${keyBasePath}.r1cs -p ${keyBasePath}.params -w ${dir}/witness.json -r ${dir}/proof.json -o ${dir}/public.json`,
|
||||||
)
|
)
|
||||||
|
// todo catch inconsistent input during witness generation
|
||||||
|
await exec(`zkutil verify -p ${keyBasePath}.params -r ${dir}/proof.json -i ${dir}/public.json`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(out, e)
|
console.log(out, e)
|
||||||
throw e
|
throw e
|
||||||
|
29
src/utils.js
29
src/utils.js
@ -1,7 +1,7 @@
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const ethers = require('ethers')
|
const ethers = require('ethers')
|
||||||
const BigNumber = ethers.BigNumber
|
const BigNumber = ethers.BigNumber
|
||||||
const { poseidon } = require('circomlib')
|
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])
|
||||||
@ -12,13 +12,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, relayer, encryptedOutput1, encryptedOutput2}) {
|
||||||
|
const abi = new ethers.utils.AbiCoder()
|
||||||
|
|
||||||
const encodedData = abi.encode(
|
const encodedData = abi.encode(
|
||||||
['address', 'address', 'bytes', 'bytes'],
|
['tuple(address recipient,address relayer,bytes encryptedOutput1,bytes encryptedOutput2)'],
|
||||||
[toFixedHex(recipient, 20), toFixedHex(relayer, 20), encryptedOutput1, encryptedOutput2],
|
[{
|
||||||
|
recipient: toFixedHex(recipient, 20),
|
||||||
|
relayer: toFixedHex(relayer, 20),
|
||||||
|
encryptedOutput1: encryptedOutput1,
|
||||||
|
encryptedOutput2: encryptedOutput2,
|
||||||
|
}],
|
||||||
)
|
)
|
||||||
const hash = ethers.utils.keccak256(encodedData)
|
const hash = ethers.utils.keccak256(encodedData)
|
||||||
|
|
||||||
return BigNumber.from(hash).mod(FIELD_SIZE)
|
return BigNumber.from(hash).mod(FIELD_SIZE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,8 +32,8 @@ function getExtDataHash({ recipient, relayer, encryptedOutput1, encryptedOutput2
|
|||||||
const toFixedHex = (number, length = 32) =>
|
const toFixedHex = (number, length = 32) =>
|
||||||
'0x' +
|
'0x' +
|
||||||
(number instanceof Buffer
|
(number instanceof Buffer
|
||||||
? number.toString('hex')
|
? number.toString('hex')
|
||||||
: BigNumber.from(number).toHexString().slice(2)
|
: BigNumber.from(number).toHexString().slice(2)
|
||||||
).padStart(length * 2, '0')
|
).padStart(length * 2, '0')
|
||||||
|
|
||||||
const toBuffer = (value, length) =>
|
const toBuffer = (value, length) =>
|
||||||
@ -39,18 +45,9 @@ const toBuffer = (value, length) =>
|
|||||||
'hex',
|
'hex',
|
||||||
)
|
)
|
||||||
|
|
||||||
function bitsToNumber(bits) {
|
|
||||||
let result = 0
|
|
||||||
for (const item of bits.slice().reverse()) {
|
|
||||||
result = (result << 1) + item
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
FIELD_SIZE,
|
FIELD_SIZE,
|
||||||
randomBN,
|
randomBN,
|
||||||
bitsToNumber,
|
|
||||||
toFixedHex,
|
toFixedHex,
|
||||||
toBuffer,
|
toBuffer,
|
||||||
poseidonHash,
|
poseidonHash,
|
||||||
|
46
src/utxo.js
Normal file
46
src/utxo.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const { ethers } = require('hardhat')
|
||||||
|
const { BigNumber } = ethers
|
||||||
|
const { randomBN, poseidonHash } = require('./utils')
|
||||||
|
|
||||||
|
function fromPrivkey(privkey) {
|
||||||
|
return {
|
||||||
|
privkey,
|
||||||
|
pubkey: poseidonHash([privkey]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Utxo {
|
||||||
|
constructor({amount, pubkey, privkey, blinding, index} = {}) {
|
||||||
|
if (!pubkey) {
|
||||||
|
if (privkey) {
|
||||||
|
pubkey = fromPrivkey(privkey).pubkey
|
||||||
|
} else {
|
||||||
|
({pubkey, privkey} = fromPrivkey(randomBN()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.amount = BigNumber.from(amount || 0);
|
||||||
|
this.blinding = blinding || randomBN();
|
||||||
|
this.pubkey = pubkey;
|
||||||
|
this.privkey = privkey;
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommitment() {
|
||||||
|
if (!this._commitment) {
|
||||||
|
this._commitment = poseidonHash([this.amount, this.blinding, this.pubkey])
|
||||||
|
}
|
||||||
|
return this._commitment
|
||||||
|
}
|
||||||
|
|
||||||
|
getNullifier() {
|
||||||
|
if (!this._nullifier) {
|
||||||
|
if (this.amount > 0 && (!this.index || !this.privkey)) {
|
||||||
|
throw new Error('Can not compute nullifier without utxo index or private key')
|
||||||
|
}
|
||||||
|
this._nullifier = poseidonHash([this.getCommitment(), this.index || 0, this.privkey || 0])
|
||||||
|
}
|
||||||
|
return this._nullifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Utxo
|
Loading…
Reference in New Issue
Block a user