utxo class, separate private key for each input

This commit is contained in:
poma 2021-06-08 21:50:34 +03:00
parent 238431233b
commit bd2252afa3
No known key found for this signature in database
GPG Key ID: BA20CB01FE165657
6 changed files with 164 additions and 267 deletions

View File

@ -26,11 +26,10 @@ template Transaction(levels, zeroLeaf) {
signal input fee;
signal input extDataHash;
signal private input privateKey;
// data for 2 transaction inputs
signal private input inAmount[2];
signal private input inBlinding[2];
signal private input inPrivateKey[2];
signal private input inPathIndices[2];
signal private input inPathElements[2][levels];
@ -42,6 +41,7 @@ template Transaction(levels, zeroLeaf) {
signal private input outPathElements[levels - 1];
component inUtxoHasher[2];
component inKeypair[2];
component outUtxoHasher[2];
component nullifierHasher[2];
component checkRoot[2]
@ -49,20 +49,20 @@ template Transaction(levels, zeroLeaf) {
component inAmountCheck[2];
component outAmountCheck[2];
component keypair = Keypair();
keypair.privateKey <== privateKey;
// verify correctness of transaction inputs
for (var tx = 0; tx < 2; tx++) {
inKeypair[tx] = Keypair();
inKeypair[tx].privateKey <== inPrivateKey[tx];
inUtxoHasher[tx] = TransactionHasher();
inUtxoHasher[tx].amount <== inAmount[tx];
inUtxoHasher[tx].blinding <== inBlinding[tx];
inUtxoHasher[tx].publicKey <== keypair.publicKey;
inUtxoHasher[tx].publicKey <== inKeypair[tx].publicKey;
nullifierHasher[tx] = NullifierHasher();
nullifierHasher[tx].commitment <== inUtxoHasher[tx].commitment;
nullifierHasher[tx].merklePath <== inPathIndices[tx];
nullifierHasher[tx].privateKey <== keypair.privateKey;
nullifierHasher[tx].privateKey <== inPrivateKey[tx];
nullifierHasher[tx].nullifier === inputNullifier[tx];
tree[tx] = MerkleTree(levels);

View File

@ -29,8 +29,8 @@ contract TornadoPool is ReentrancyGuard {
IVerifier public verifier;
struct ExtData {
address payable _recipient;
address payable _relayer;
address payable recipient;
address payable relayer;
bytes encryptedOutput1;
bytes encryptedOutput2;
}
@ -87,11 +87,11 @@ contract TornadoPool is ReentrancyGuard {
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");
_extData._recipient.transfer(uint256(-extAmount));
_extData.recipient.transfer(uint256(-extAmount));
}
if (_fee > 0) {
_extData._relayer.transfer(_fee);
_extData.relayer.transfer(_fee);
}
// todo enforce currentCommitmentIndex value in snark

View File

@ -3,127 +3,88 @@ const MerkleTree = require('fixed-merkle-tree')
const Web3 = require('web3')
const { ethers } = require('hardhat')
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
const { prove } = require('./prover')
const FIELD_SIZE = '21888242871839275222246405745257275088548364400416034343698204186575808495617'
const MERKLE_TREE_HEIGHT = 5
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() {
console.log('Getting contract state...')
const events = await contract.getPastEvents('NewCommitment', { fromBlock: 0, toBlock: 'latest' })
const leaves = events
.sort((a, b) => a.returnValues.index - b.returnValues.index) // todo sort by event date
.map((e) => toFixedHex(e.returnValues.commitment))
console.log('leaves', leaves)
// console.log('leaves', leaves)
return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 })
}
async function insertOutput(tree, output) {
await tree.insert(output.commitment)
let { pathElements, pathIndices } = await tree.path(tree.elements().length - 1)
output.merklePathIndices = pathIndices
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)
async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, relayer }) {
// todo shuffle inputs and outputs
if (inputs.length !== 2 || outputs.length !== 2 ) {
throw new Error('Unsupported number of inputs/outputs')
}
await insertOutput(tree, output1)
await insertOutput(tree, output2)
let inputMerklePathIndices = []
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 = {
root: oldRoot,
newRoot: tree.root(),
inputNullifier: [input1.nullifier, input2.nullifier],
outputCommitment: [outputs1.commitment, outputs2.commitment],
inputNullifier: inputs.map(x => x.getNullifier()),
outputCommitment: outputs.map(x => x.getCommitment()),
extAmount,
fee,
extData,
// private inputs
privateKey: inputs1.privkey, // TODO make sure you use the right one when you shuffle inputs
extDataHash,
// 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],
inAmount: inputs.map(x => x.amount),
inPrivateKey: inputs.map(x => x.privkey),
inBlinding: inputs.map(x => x.blinding),
inPathIndices: inputMerklePathIndices,
inPathElements: inputMerklePathElements,
// 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)],
outAmount: outputs.map(x => x.amount),
outBlinding: outputs.map(x => x.blinding),
outPubkey: outputs.map(x => x.pubkey),
outPathIndices: outputIndex >> 1,
outPathElements: outputPath,
}
console.log('SNARK input', input)
//console.log('SNARK input', input)
console.log('Generating SNARK proof...')
const proof = await prove(input, './artifacts/circuits/transaction')
@ -131,12 +92,14 @@ async function getProof({ input1, input2, output1, output2, tree, extAmount, fee
const args = [
toFixedHex(input.root),
toFixedHex(input.newRoot),
[toFixedHex(input1.nullifier), toFixedHex(input2.nullifier)],
[toFixedHex(output1.commitment), toFixedHex(output2.commitment)],
toFixedHex(0),
toFixedHex(input.fee),
toFixedHex(extData), // extData hash actually
inputs.map(x => toFixedHex(x.getNullifier())),
outputs.map(x => toFixedHex(x.getCommitment())),
toFixedHex(extAmount),
toFixedHex(fee),
extData,
toFixedHex(extDataHash),
]
// console.log('Solidity args', args)
return {
proof,
@ -146,16 +109,13 @@ async function getProof({ input1, input2, output1, output2, tree, extAmount, fee
async function deposit() {
const amount = 1e6
const tree = await buildMerkleTree()
const keypair = randomKeypair()
const tx = createDeposit(amount, keypair)
const inputs = [new Utxo(), new Utxo()]
const outputs = [new Utxo({ amount }), new Utxo()]
const { proof, args } = await getProof({
input1: tx.inputs[0],
input2: tx.inputs[1],
output1: tx.outputs[1],
output2: tx.outputs[1],
tree,
inputs,
outputs,
tree: await buildMerkleTree(),
extAmount: amount,
fee: 0,
recipient: 0,
@ -167,155 +127,47 @@ async function deposit() {
.transaction(proof, ...args)
.send({ value: amount, from: web3.eth.defaultAccount, gas: 1e6 })
console.log(`Receipt ${receipt.transactionHash}`)
return tx.outputs[0]
return outputs[0]
}
async function transact(txOutput) {
console.log('txOutput', txOutput)
const tree = await buildMerkleTree()
console.log('tree', tree)
const oldRoot = await tree.root()
const keypair = randomKeypair()
async function transact(utxo) {
const inputs = [utxo, new Utxo()]
const outputs = [
new Utxo({ amount: utxo.amount / 4 }),
new Utxo({ amount: utxo.amount * 3 / 4, privkey: utxo.privkey}),
]
const index = await tree.indexOf(toFixedHex(txOutput.commitment))
console.log('index', index)
const { pathElements, pathIndices } = await tree.path(index)
console.log('pathIndices', pathIndices)
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],
const { proof, args } = await getProof({
inputs,
outputs,
tree: await buildMerkleTree(),
extAmount: 0,
fee: 0,
recipient: 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...')
const receipt = await contract.methods
.transaction(proof, ...args)
.send({ from: web3.eth.defaultAccount, gas: 1e6 })
console.log(`Receipt ${receipt.transactionHash}`)
return tx.outputs[0]
return outputs[0]
}
async function withdraw(txOutput) {
console.log('txOutput', txOutput)
const tree = await buildMerkleTree()
const oldRoot = await tree.root()
async function withdraw(utxo) {
const inputs = [utxo, new Utxo()]
const outputs = [new Utxo(), new Utxo()]
const index = await tree.indexOf(toFixedHex(txOutput.commitment))
console.log('index', index)
const { pathElements, pathIndices } = await tree.path(index)
console.log('pathIndices', pathIndices)
txOutput.merklePathElements = pathElements
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)),
const { proof, args } = await getProof({
inputs,
outputs,
tree: await buildMerkleTree(),
extAmount: FIELD_SIZE.sub(utxo.amount),
fee: 0,
recipient: '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48',
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...')
const receipt = await contract.methods
@ -333,12 +185,12 @@ async function main() {
})
netId = await web3.eth.net.getId()
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]
const txOutput = await deposit()
const txOutput1 = await transact(txOutput)
await withdraw(txOutput1)
const utxo1 = await deposit()
const utxo2 = await transact(utxo1)
await withdraw(utxo2)
}
main()

View File

@ -8,7 +8,7 @@ const exec = util.promisify(require('child_process').exec)
function prove(input, keyBasePath) {
input = utils.stringifyBigInts(input)
console.log('input', input)
// console.log('input', input)
return tmp.dir().then(async (dir) => {
dir = dir.path
let out
@ -28,6 +28,8 @@ function prove(input, keyBasePath) {
out = await exec(
`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) {
console.log(out, e)
throw e

View File

@ -1,7 +1,7 @@
const crypto = require('crypto')
const ethers = require('ethers')
const BigNumber = ethers.BigNumber
const { poseidon } = require('circomlib')
const {poseidon} = require('circomlib')
const poseidonHash = (items) => BigNumber.from(poseidon(items).toString())
const poseidonHash2 = (a, b) => poseidonHash([a, b])
@ -12,13 +12,19 @@ const FIELD_SIZE = BigNumber.from(
/** Generate random number of specified byte length */
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(
['address', 'address', 'bytes', 'bytes'],
[toFixedHex(recipient, 20), toFixedHex(relayer, 20), encryptedOutput1, encryptedOutput2],
['tuple(address recipient,address relayer,bytes encryptedOutput1,bytes encryptedOutput2)'],
[{
recipient: toFixedHex(recipient, 20),
relayer: toFixedHex(relayer, 20),
encryptedOutput1: encryptedOutput1,
encryptedOutput2: encryptedOutput2,
}],
)
const hash = ethers.utils.keccak256(encodedData)
return BigNumber.from(hash).mod(FIELD_SIZE)
}
@ -26,8 +32,8 @@ function getExtDataHash({ recipient, relayer, encryptedOutput1, encryptedOutput2
const toFixedHex = (number, length = 32) =>
'0x' +
(number instanceof Buffer
? number.toString('hex')
: BigNumber.from(number).toHexString().slice(2)
? number.toString('hex')
: BigNumber.from(number).toHexString().slice(2)
).padStart(length * 2, '0')
const toBuffer = (value, length) =>
@ -39,18 +45,9 @@ const toBuffer = (value, length) =>
'hex',
)
function bitsToNumber(bits) {
let result = 0
for (const item of bits.slice().reverse()) {
result = (result << 1) + item
}
return result
}
module.exports = {
FIELD_SIZE,
randomBN,
bitsToNumber,
toFixedHex,
toBuffer,
poseidonHash,

46
src/utxo.js Normal file
View 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