mirror of
https://github.com/tornadocash/tornado-nova
synced 2024-02-02 14:53:56 +01:00
encrypion and full test
This commit is contained in:
parent
5bdc8d7871
commit
18571118d7
5
TODO
5
TODO
@ -7,3 +7,8 @@
|
|||||||
* design
|
* design
|
||||||
* race condition ? (sequencer or something)
|
* race condition ? (sequencer or something)
|
||||||
* the current trusted setup is not secure
|
* 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)
|
||||||
|
41
src/index.js
41
src/index.js
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
const MerkleTree = require('fixed-merkle-tree')
|
const MerkleTree = require('fixed-merkle-tree')
|
||||||
const { ethers } = require('hardhat')
|
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 Utxo = require('./utxo')
|
||||||
|
|
||||||
const { prove } = require('./prover')
|
const { prove } = require('./prover')
|
||||||
@ -29,7 +29,7 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
|
|||||||
if (input.amount > 0) {
|
if (input.amount > 0) {
|
||||||
const index = tree.indexOf(toFixedHex(input.getCommitment()))
|
const index = tree.indexOf(toFixedHex(input.getCommitment()))
|
||||||
if (index < 0) {
|
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)
|
inputMerklePathIndices.push(index)
|
||||||
inputMerklePathElements.push(tree.path(index).pathElements)
|
inputMerklePathElements.push(tree.path(index).pathElements)
|
||||||
@ -52,8 +52,12 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
|
|||||||
const extData = {
|
const extData = {
|
||||||
recipient: toFixedHex(recipient, 20),
|
recipient: toFixedHex(recipient, 20),
|
||||||
relayer: toFixedHex(relayer, 20),
|
relayer: toFixedHex(relayer, 20),
|
||||||
encryptedOutput1: '0xff',
|
encryptedOutput1: packEncryptedMessage(
|
||||||
encryptedOutput2: '0xff',
|
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)
|
const extDataHash = getExtDataHash(extData)
|
||||||
@ -104,16 +108,15 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deposit({ tornadoPool }) {
|
async function deposit({ tornadoPool, utxo }) {
|
||||||
const amount = 1e6
|
|
||||||
const inputs = [new Utxo(), new Utxo()]
|
const inputs = [new Utxo(), new Utxo()]
|
||||||
const outputs = [new Utxo({ amount }), new Utxo()]
|
const outputs = [utxo, new Utxo()]
|
||||||
|
|
||||||
const { proof, args } = await getProof({
|
const { proof, args } = await getProof({
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
tree: await buildMerkleTree({ tornadoPool }),
|
tree: await buildMerkleTree({ tornadoPool }),
|
||||||
extAmount: amount,
|
extAmount: utxo.amount,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
recipient: 0,
|
recipient: 0,
|
||||||
relayer: 0,
|
relayer: 0,
|
||||||
@ -121,7 +124,7 @@ async function deposit({ tornadoPool }) {
|
|||||||
|
|
||||||
console.log('Sending deposit transaction...')
|
console.log('Sending deposit transaction...')
|
||||||
const receipt = await tornadoPool.transaction(proof, ...args, {
|
const receipt = await tornadoPool.transaction(proof, ...args, {
|
||||||
value: amount,
|
value: utxo.amount,
|
||||||
gasLimit: 1e6,
|
gasLimit: 1e6,
|
||||||
})
|
})
|
||||||
console.log(`Receipt ${receipt.hash}`)
|
console.log(`Receipt ${receipt.hash}`)
|
||||||
@ -130,7 +133,7 @@ async function deposit({ tornadoPool }) {
|
|||||||
|
|
||||||
async function merge({ tornadoPool }) {
|
async function merge({ tornadoPool }) {
|
||||||
const amount = 1e6
|
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 outputs = [new Utxo({ amount }), new Utxo()]
|
||||||
|
|
||||||
const { proof, args } = await getProof({
|
const { proof, args } = await getProof({
|
||||||
@ -143,7 +146,6 @@ async function merge({ tornadoPool }) {
|
|||||||
relayer: 0,
|
relayer: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Sending merge transaction...', proof, args)
|
|
||||||
const receipt = await tornadoPool.transaction(proof, ...args, {
|
const receipt = await tornadoPool.transaction(proof, ...args, {
|
||||||
value: amount,
|
value: amount,
|
||||||
gasLimit: 1e6,
|
gasLimit: 1e6,
|
||||||
@ -152,12 +154,9 @@ async function merge({ tornadoPool }) {
|
|||||||
return outputs[0]
|
return outputs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function transact({ tornadoPool, utxo }) {
|
async function transact({ tornadoPool, input, output }) {
|
||||||
const inputs = [utxo, new Utxo()]
|
const inputs = [input, new Utxo()]
|
||||||
const outputs = [
|
const outputs = [output, new Utxo()]
|
||||||
new Utxo({ amount: utxo.amount / 4 }),
|
|
||||||
new Utxo({ amount: (utxo.amount * 3) / 4, keypair: utxo.keypair }),
|
|
||||||
]
|
|
||||||
|
|
||||||
const { proof, args } = await getProof({
|
const { proof, args } = await getProof({
|
||||||
inputs,
|
inputs,
|
||||||
@ -175,15 +174,15 @@ async function transact({ tornadoPool, utxo }) {
|
|||||||
return outputs[0]
|
return outputs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withdraw({ tornadoPool, utxo, recipient }) {
|
async function withdraw({ tornadoPool, input, change, recipient }) {
|
||||||
const inputs = [utxo, new Utxo()]
|
const inputs = [input, new Utxo()]
|
||||||
const outputs = [new Utxo(), new Utxo()]
|
const outputs = [change, new Utxo()]
|
||||||
|
|
||||||
const { proof, args } = await getProof({
|
const { proof, args } = await getProof({
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
tree: await buildMerkleTree({ tornadoPool }),
|
tree: await buildMerkleTree({ tornadoPool }),
|
||||||
extAmount: FIELD_SIZE.sub(utxo.amount),
|
extAmount: FIELD_SIZE.sub(input.amount.sub(change.amount)),
|
||||||
fee: 0,
|
fee: 0,
|
||||||
recipient,
|
recipient,
|
||||||
relayer: 0,
|
relayer: 0,
|
||||||
|
@ -2,18 +2,21 @@ const { encrypt, decrypt, getEncryptionPublicKey } = require('eth-sig-util')
|
|||||||
const { ethers } = require('hardhat')
|
const { ethers } = require('hardhat')
|
||||||
const { BigNumber } = ethers
|
const { BigNumber } = ethers
|
||||||
const { randomBN, poseidonHash, toFixedHex } = require('./utils')
|
const { randomBN, poseidonHash, toFixedHex } = require('./utils')
|
||||||
|
const BNjs = require('bn.js')
|
||||||
|
|
||||||
class Keypair {
|
class Keypair {
|
||||||
constructor(privkey = ethers.Wallet.createRandom().privateKey) {
|
constructor(privkey = ethers.Wallet.createRandom().privateKey) {
|
||||||
this.privkey = privkey
|
this.privkey = privkey
|
||||||
console.log(privkey)
|
|
||||||
this.pubkey = poseidonHash([this.privkey])
|
this.pubkey = poseidonHash([this.privkey])
|
||||||
this.encryptionKey = getEncryptionPublicKey(privkey.slice(2))
|
this.encryptionKey = getEncryptionPublicKey(privkey.slice(2))
|
||||||
console.log('enc key', this.encryptionKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
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) {
|
static fromString(str) {
|
||||||
@ -26,24 +29,21 @@ class Keypair {
|
|||||||
return Object.assign(new Keypair(), {
|
return Object.assign(new Keypair(), {
|
||||||
privkey: null,
|
privkey: null,
|
||||||
pubkey: BigNumber.from('0x' + str.slice(0, 64)),
|
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 }) {
|
encrypt({ blinding, amount }) {
|
||||||
console.log(BigNumber.from(blinding).toHexString())
|
|
||||||
const bytes = Buffer.concat([
|
const bytes = Buffer.concat([
|
||||||
Buffer.from(BigNumber.from(blinding).toHexString(), 0, 31),
|
new BNjs(blinding.toString()).toBuffer('be', 31),
|
||||||
Buffer.from(BigNumber.from(amount).toHexString(), 0, 31),
|
new BNjs(amount.toString()).toBuffer('be', 31),
|
||||||
])
|
])
|
||||||
console.log(bytes)
|
|
||||||
return encrypt(this.encryptionKey, { data: bytes.toString('base64') }, 'x25519-xsalsa20-poly1305')
|
return encrypt(this.encryptionKey, { data: bytes.toString('base64') }, 'x25519-xsalsa20-poly1305')
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypt(data) {
|
decrypt(data) {
|
||||||
const decryptedMessage = decrypt(data, this.privkey.slice(2))
|
const decryptedMessage = decrypt(data, this.privkey.slice(2))
|
||||||
const buf = Buffer.from(decryptedMessage, 'base64')
|
const buf = Buffer.from(decryptedMessage, 'base64')
|
||||||
console.log(buf)
|
|
||||||
return {
|
return {
|
||||||
blinding: BigNumber.from('0x' + buf.slice(0, 31).toString('hex')),
|
blinding: BigNumber.from('0x' + buf.slice(0, 31).toString('hex')),
|
||||||
amount: BigNumber.from('0x' + buf.slice(31, 62).toString('hex')),
|
amount: BigNumber.from('0x' + buf.slice(31, 62).toString('hex')),
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
/* global ethers */
|
/* global ethers */
|
||||||
const { expect, should } = require('chai')
|
const { expect, should } = require('chai')
|
||||||
should()
|
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 MERKLE_TREE_HEIGHT = 5
|
||||||
const MerkleTree = require('fixed-merkle-tree')
|
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 { deposit, transact, withdraw, merge } = require('../src/index')
|
||||||
const Keypair = require('../src/keypair')
|
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', () => {
|
describe('TornadoPool', () => {
|
||||||
let snapshotId, tornadoPool
|
let snapshotId, tornadoPool
|
||||||
|
|
||||||
@ -47,19 +41,89 @@ describe('TornadoPool', () => {
|
|||||||
snapshotId = await takeSnapshot()
|
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 () {
|
it('should deposit, transact and withdraw', async function () {
|
||||||
const utxo1 = await deposit({ tornadoPool })
|
/// deposit phase
|
||||||
const utxo2 = await transact({ tornadoPool, utxo: utxo1 })
|
// 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'
|
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)
|
expect(bal).to.be.gt(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should work with 16 inputs', async function () {
|
it('should work with 16 inputs', async function () {
|
||||||
const utxo1 = await merge({tornadoPool})
|
const utxo1 = await merge({ tornadoPool })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user