From 18571118d77eb934276ce110c111b48c412429ef Mon Sep 17 00:00:00 2001 From: Alexey Date: Tue, 15 Jun 2021 23:02:59 +0300 Subject: [PATCH] encrypion and full test --- TODO | 5 +++ src/index.js | 41 +++++++++--------- src/keypair.js | 18 ++++---- test/full.test.js | 106 +++++++++++++++++++++++++++++++++++++--------- 4 files changed, 119 insertions(+), 51 deletions(-) diff --git a/TODO b/TODO index 3a1eb18..a8c624c 100644 --- a/TODO +++ b/TODO @@ -7,3 +7,8 @@ * design * race condition ? (sequencer or something) * the current trusted setup is not secure + +How we do transaction inside pool of A amount. +1. sort inputs by amount +2. try to take 1 or 2 smallest inputs to satisfy A amount. Get 16 inputs if it's not possible using the same way +3. Also you can always use transaction to merge your inputs with change (especially in 16 inputs case) diff --git a/src/index.js b/src/index.js index 310073e..815d3b1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ /* eslint-disable no-console */ const MerkleTree = require('fixed-merkle-tree') const { ethers } = require('hardhat') -const { toFixedHex, poseidonHash2, getExtDataHash, FIELD_SIZE } = require('./utils') +const { toFixedHex, poseidonHash2, getExtDataHash, FIELD_SIZE, packEncryptedMessage } = require('./utils') const Utxo = require('./utxo') const { prove } = require('./prover') @@ -29,7 +29,7 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela if (input.amount > 0) { const index = tree.indexOf(toFixedHex(input.getCommitment())) if (index < 0) { - throw new Error(`Input commitment ${input.getCommitment()} was not found`) + throw new Error(`Input commitment ${toFixedHex(input.getCommitment())} was not found`) } inputMerklePathIndices.push(index) inputMerklePathElements.push(tree.path(index).pathElements) @@ -52,8 +52,12 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela const extData = { recipient: toFixedHex(recipient, 20), relayer: toFixedHex(relayer, 20), - encryptedOutput1: '0xff', - encryptedOutput2: '0xff', + encryptedOutput1: packEncryptedMessage( + outputs[0].keypair.encrypt({ blinding: outputs[0].blinding, amount: outputs[0].amount }), + ), + encryptedOutput2: packEncryptedMessage( + outputs[1].keypair.encrypt({ blinding: outputs[1].blinding, amount: outputs[1].amount }), + ), } const extDataHash = getExtDataHash(extData) @@ -104,16 +108,15 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela } } -async function deposit({ tornadoPool }) { - const amount = 1e6 +async function deposit({ tornadoPool, utxo }) { const inputs = [new Utxo(), new Utxo()] - const outputs = [new Utxo({ amount }), new Utxo()] + const outputs = [utxo, new Utxo()] const { proof, args } = await getProof({ inputs, outputs, tree: await buildMerkleTree({ tornadoPool }), - extAmount: amount, + extAmount: utxo.amount, fee: 0, recipient: 0, relayer: 0, @@ -121,7 +124,7 @@ async function deposit({ tornadoPool }) { console.log('Sending deposit transaction...') const receipt = await tornadoPool.transaction(proof, ...args, { - value: amount, + value: utxo.amount, gasLimit: 1e6, }) console.log(`Receipt ${receipt.hash}`) @@ -130,7 +133,7 @@ async function deposit({ tornadoPool }) { async function merge({ tornadoPool }) { const amount = 1e6 - const inputs = new Array(16).fill(0).map(_ => new Utxo()) + const inputs = new Array(16).fill(0).map((_) => new Utxo()) const outputs = [new Utxo({ amount }), new Utxo()] const { proof, args } = await getProof({ @@ -143,7 +146,6 @@ async function merge({ tornadoPool }) { relayer: 0, }) - console.log('Sending merge transaction...', proof, args) const receipt = await tornadoPool.transaction(proof, ...args, { value: amount, gasLimit: 1e6, @@ -152,12 +154,9 @@ async function merge({ tornadoPool }) { return outputs[0] } -async function transact({ tornadoPool, utxo }) { - const inputs = [utxo, new Utxo()] - const outputs = [ - new Utxo({ amount: utxo.amount / 4 }), - new Utxo({ amount: (utxo.amount * 3) / 4, keypair: utxo.keypair }), - ] +async function transact({ tornadoPool, input, output }) { + const inputs = [input, new Utxo()] + const outputs = [output, new Utxo()] const { proof, args } = await getProof({ inputs, @@ -175,15 +174,15 @@ async function transact({ tornadoPool, utxo }) { return outputs[0] } -async function withdraw({ tornadoPool, utxo, recipient }) { - const inputs = [utxo, new Utxo()] - const outputs = [new Utxo(), new Utxo()] +async function withdraw({ tornadoPool, input, change, recipient }) { + const inputs = [input, new Utxo()] + const outputs = [change, new Utxo()] const { proof, args } = await getProof({ inputs, outputs, tree: await buildMerkleTree({ tornadoPool }), - extAmount: FIELD_SIZE.sub(utxo.amount), + extAmount: FIELD_SIZE.sub(input.amount.sub(change.amount)), fee: 0, recipient, relayer: 0, diff --git a/src/keypair.js b/src/keypair.js index 5bb09e5..4084335 100644 --- a/src/keypair.js +++ b/src/keypair.js @@ -2,18 +2,21 @@ const { encrypt, decrypt, getEncryptionPublicKey } = require('eth-sig-util') const { ethers } = require('hardhat') const { BigNumber } = ethers const { randomBN, poseidonHash, toFixedHex } = require('./utils') +const BNjs = require('bn.js') class Keypair { constructor(privkey = ethers.Wallet.createRandom().privateKey) { this.privkey = privkey - console.log(privkey) this.pubkey = poseidonHash([this.privkey]) this.encryptionKey = getEncryptionPublicKey(privkey.slice(2)) - console.log('enc key', this.encryptionKey) } toString() { - return toFixedHex(this.pubkey) + toFixedHex(this.encryptionKey).slice(2) + return toFixedHex(this.pubkey) + Buffer.from(this.encryptionKey, 'base64').toString('hex') + } + + address() { + return this.toString() } static fromString(str) { @@ -26,24 +29,21 @@ class Keypair { return Object.assign(new Keypair(), { privkey: null, pubkey: BigNumber.from('0x' + str.slice(0, 64)), - encryptionKey: BigNumber.from('0x' + str.slice(64, 128)), + encryptionKey: Buffer.from(str.slice(64, 128), 'hex').toString('base64'), }) } encrypt({ blinding, amount }) { - console.log(BigNumber.from(blinding).toHexString()) const bytes = Buffer.concat([ - Buffer.from(BigNumber.from(blinding).toHexString(), 0, 31), - Buffer.from(BigNumber.from(amount).toHexString(), 0, 31), + new BNjs(blinding.toString()).toBuffer('be', 31), + new BNjs(amount.toString()).toBuffer('be', 31), ]) - console.log(bytes) return encrypt(this.encryptionKey, { data: bytes.toString('base64') }, 'x25519-xsalsa20-poly1305') } decrypt(data) { const decryptedMessage = decrypt(data, this.privkey.slice(2)) const buf = Buffer.from(decryptedMessage, 'base64') - console.log(buf) return { blinding: BigNumber.from('0x' + buf.slice(0, 31).toString('hex')), amount: BigNumber.from('0x' + buf.slice(31, 62).toString('hex')), diff --git a/test/full.test.js b/test/full.test.js index 8da300f..4a83fa6 100644 --- a/test/full.test.js +++ b/test/full.test.js @@ -1,8 +1,17 @@ /* global ethers */ const { expect, should } = require('chai') should() +const { BigNumber } = ethers -const { poseidonHash2, toFixedHex, takeSnapshot, revertSnapshot } = require('../src/utils') +const { + poseidonHash2, + toFixedHex, + takeSnapshot, + revertSnapshot, + packEncryptedMessage, + unpackEncryptedMessage, +} = require('../src/utils') +const Utxo = require('../src/utxo') const MERKLE_TREE_HEIGHT = 5 const MerkleTree = require('fixed-merkle-tree') @@ -10,21 +19,6 @@ const MerkleTree = require('fixed-merkle-tree') const { deposit, transact, withdraw, merge } = require('../src/index') const Keypair = require('../src/keypair') -describe.only('Keypair', () => { - it('should work', () => { - const blinding = 3 - const amount = 5 - const keypair = new Keypair() - - const cyphertext = keypair.encrypt({ blinding, amount}) - console.log(cyphertext) - const result = keypair.decrypt(cyphertext) - console.log(result, result.blinding.toString()) - expect(result.blinding).to.be.equal(blinding) - expect(result.amount).to.be.equal(amount) - }) -}) - describe('TornadoPool', () => { let snapshotId, tornadoPool @@ -47,19 +41,89 @@ describe('TornadoPool', () => { snapshotId = await takeSnapshot() }) + it('encryp -> pack -> unpack -> decrypt should work', () => { + const blinding = 3 + const amount = 5 + const keypair = new Keypair() + + const cyphertext = keypair.encrypt({ blinding, amount }) + + const packedMessage = packEncryptedMessage(cyphertext) + + const unpackedMessage = unpackEncryptedMessage(packedMessage) + + const result = keypair.decrypt(unpackedMessage) + + expect(result.blinding).to.be.equal(blinding) + expect(result.amount).to.be.equal(amount) + }) + it('should deposit, transact and withdraw', async function () { - const utxo1 = await deposit({ tornadoPool }) - const utxo2 = await transact({ tornadoPool, utxo: utxo1 }) + /// deposit phase + // Alice deposits into tornado pool + const amount = BigNumber.from('10000000') + const alicePrivateKey = ethers.Wallet.createRandom().privateKey // the private key we use for snarks and encryption, not for transactions + const aliceKeypair = new Keypair(alicePrivateKey) + const depositInput = new Utxo({ amount, keypair: aliceKeypair }) + await deposit({ tornadoPool, utxo: depositInput }) + + // getting account data from chain to verify that Alice has an Input to spend now + const filter = tornadoPool.filters.NewCommitment() + let events = await tornadoPool.queryFilter(filter) + let unpackedMessage = unpackEncryptedMessage(events[0].args.encryptedOutput) + let decryptedMessage = aliceKeypair.decrypt(unpackedMessage) + let aliceInputIndex = events[0].args.index + expect(decryptedMessage.amount).to.be.equal(amount) + expect(decryptedMessage.blinding).to.be.equal(depositInput.blinding) + + /// transact phase. + // Bob gives Alice address to send some eth inside pool + const bobPrivateKey = ethers.Wallet.createRandom().privateKey + const bobKeypair = new Keypair(bobPrivateKey) + const bobAddress = bobKeypair.address() + + // but alice does not have Bob's privkey so let's build keypair without it + const bobKeypairForEncryption = Keypair.fromString(bobAddress) + + // let's build input for the shielded transaction + const aliceInput = new Utxo({ + amount, + blinding: depositInput.blinding, + index: aliceInputIndex, + keypair: aliceKeypair, + }) + const bobInput = new Utxo({ amount, keypair: bobKeypairForEncryption }) + + await transact({ tornadoPool, input: aliceInput, output: bobInput }) + + // getting account data from chain to verify that Bob has an Input to spend now + const fromBlock = await ethers.provider.getBlock() + events = await tornadoPool.queryFilter(filter, fromBlock.number) + const bobInputIndex = events[0].args.index + unpackedMessage = unpackEncryptedMessage(events[0].args.encryptedOutput) + decryptedMessage = bobKeypair.decrypt(unpackedMessage) + expect(decryptedMessage.amount).to.be.equal(amount) + expect(decryptedMessage.blinding).to.be.equal(bobInput.blinding) + + /// withdraw phase + // now Bob wants to exit the pool using a half of its funds + const bobInputForWithdraw = new Utxo({ + amount, + blinding: bobInput.blinding, + index: bobInputIndex, + keypair: bobKeypair, + }) + const bobChange = new Utxo({ amount: amount.div(2), keypair: bobKeypair }) const recipient = '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48' - await withdraw({ tornadoPool, utxo: utxo2, recipient }) + await withdraw({ tornadoPool, input: bobInputForWithdraw, change: bobChange, recipient }) - let bal = await ethers.provider.getBalance(recipient) + const bal = await ethers.provider.getBalance(recipient) expect(bal).to.be.gt(0) }) it('should work with 16 inputs', async function () { - const utxo1 = await merge({tornadoPool}) + const utxo1 = await merge({ tornadoPool }) }) afterEach(async () => {