diff --git a/.gitignore b/.gitignore index e68e0db..5d605ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules .env .idea -backup-tornado-* \ No newline at end of file +backup-tornado-* +backup-tornadoInvoice-* diff --git a/README.md b/README.md index 494d8a2..7094498 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,14 @@ Current cli version doesn't support [Anonymity Mining](https://tornado-cash.medi ### How to install tornado cli Download and install [node.js](https://nodejs.org/en/download/). +You also need to install C++ build tools in order to do 'npm install', for more information please checkout https://github.com/nodejs/node-gyp#on-unix. + +For Windows: https://stackoverflow.com/a/64224475 + +For MacOS: Install XCode Command Line Tools + +For Linux: Install make & gcc, for ubuntu `$ sudo apt-get install -y build-essentials` + If you have git installed on your system, clone the master branch. ```bash @@ -57,6 +65,38 @@ Transaction mined in block 17036120 Done ``` +### (Optional) Creating Deposit Notes & Invoices offline +One of the main features of tornado-cli is that it supports creating deposit notes & invoices inside the offline computing environment. + +After the private-key like notes are backed up somewhere safe, you can copy the created deposit invoices and use them to create new deposit transaction on online environment. + +To create deposit notes with `createNote` command. + +```bash +$ node cli.js createNote ETH 0.1 5 +Your note: tornado-eth-0.1-5-0x1d9771a7b9f8b6c03d33116208ce8db1aa559d33e65d22dd2ff78375fc6b635f930536d2432b4bde0178c72cfc79d6b27023c5d9de60985f186b34c18c00 +Your invoice for deposit: tornadoInvoice-eth-0.1-5-0x1b680c7dda0c2dd1b85f0fe126d49b16ed594b3cd6d5114db5f4593877a6b84f +Backed up deposit note as ./backup-tornado-eth-0.1-5-0x1d9771a7.txt +Backed up invoice as ./backup-tornadoInvoice-eth-0.1-5-0x1b680c7d.txt +``` + +To create corresponding deposit transaction you only need invoice value, so that the deposit note could be stored without exposed anywhere. + +```bash +node cli.js depositInvoice tornadoInvoice-eth-0.1-5-0x1b680c7dda0c2dd1b85f0fe126d49b16ed594b3cd6d5114db5f4593877a6b84f --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --tor 9150 +Using tor network +Your remote IP address is xx.xx.xx.xx from xx. +Creating ETH 0.1 deposit for Goerli network. +Using supplied invoice for deposit +Tornado contract balance is xxx.x ETH +Sender account balance is x.xxxxxxx ETH +Submitting deposit transaction +Submitting transaction to the remote node +View transaction on block explorer https://goerli.etherscan.io/tx/0x6ded443caed8d6f2666841149532c64bee149a9a8e1070ed4c91a12dd1837747 +Tornado contract balance is xxx.x ETH +Sender account balance is x.xxxxxxx ETH +``` + ### List of public rpc & relayers for withdrawal Infura API key fetched from https://rpc.info (Same one with Metamask) diff --git a/cli.js b/cli.js index 23a481c..164c484 100644 --- a/cli.js +++ b/cli.js @@ -180,27 +180,68 @@ async function backupNote({ currency, amount, netId, note, noteString }) { } } +async function backupInvoice({ currency, amount, netId, commitmentNote, invoiceString }) { + try { + await fs.writeFileSync(`./backup-tornadoInvoice-${currency}-${amount}-${netId}-${commitmentNote.slice(0, 10)}.txt`, invoiceString, 'utf8'); + console.log("Backed up invoice as", `./backup-tornadoInvoice-${currency}-${amount}-${netId}-${commitmentNote.slice(0, 10)}.txt`) + } catch (e) { + throw new Error('Writing backup invoice failed:', e) + } +} + /** - * Make a deposit + * create a deposit invoice. * @param currency Сurrency * @param amount Deposit amount */ -async function deposit({ currency, amount }) { - assert(senderAccount != null, 'Error! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you deposit'); +async function createInvoice({ currency, amount, chainId }) { const deposit = createDeposit({ nullifier: rbigint(31), secret: rbigint(31) }); const note = toHex(deposit.preimage, 62); - const noteString = `tornado-${currency}-${amount}-${netId}-${note}`; + const noteString = `tornado-${currency}-${amount}-${chainId}-${note}`; console.log(`Your note: ${noteString}`); - await backupNote({ currency, amount, netId, note, noteString }); + + const commitmentNote = toHex(deposit.commitment); + const invoiceString = `tornadoInvoice-${currency}-${amount}-${chainId}-${commitmentNote}`; + console.log(`Your invoice for deposit: ${invoiceString}`); + + await backupNote({ currency, amount, netId: chainId, note, noteString }); + await backupInvoice({ currency, amount, netId: chainId, commitmentNote, invoiceString }); + + return (noteString, invoiceString); +} + +/** + * Make a deposit + * @param currency Сurrency + * @param amount Deposit amount + */ +async function deposit({ currency, amount, commitmentNote }) { + assert(senderAccount != null, 'Error! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you deposit'); + let commitment; + if (!commitmentNote) { + console.log("Creating new random deposit note"); + const deposit = createDeposit({ + nullifier: rbigint(31), + secret: rbigint(31) + }); + const note = toHex(deposit.preimage, 62); + const noteString = `tornado-${currency}-${amount}-${netId}-${note}`; + console.log(`Your note: ${noteString}`); + await backupNote({ currency, amount, netId, note, noteString }); + commitment = toHex(deposit.commitment); + } else { + console.log("Using supplied invoice for deposit"); + commitment = toHex(commitmentNote); + } if (currency === netSymbol.toLowerCase()) { await printETHBalance({ address: tornadoContract._address, name: 'Tornado contract' }); await printETHBalance({ address: senderAccount, name: 'Sender account' }); const value = isTestRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 }); console.log('Submitting deposit transaction'); - await generateTransaction(contractAddress, tornado.methods.deposit(tornadoInstance, toHex(deposit.commitment), []).encodeABI(), value); + await generateTransaction(contractAddress, tornado.methods.deposit(tornadoInstance, commitment, []).encodeABI(), value); await printETHBalance({ address: tornadoContract._address, name: 'Tornado contract' }); await printETHBalance({ address: senderAccount, name: 'Sender account' }); } else { @@ -222,12 +263,14 @@ async function deposit({ currency, amount }) { } console.log('Submitting deposit transaction'); - await generateTransaction(contractAddress, tornado.methods.deposit(tornadoInstance, toHex(deposit.commitment), []).encodeABI()); + await generateTransaction(contractAddress, tornado.methods.deposit(tornadoInstance, commitment, []).encodeABI()); await printERC20Balance({ address: tornadoContract._address, name: 'Tornado contract' }); await printERC20Balance({ address: senderAccount, name: 'Sender account' }); } - return noteString; + if(!commitmentNote) { + return noteString; + } } /** @@ -1051,6 +1094,29 @@ function parseNote(noteString) { } } +/** + * Parses Tornado.cash deposit invoice + * @param invoiceString the note + */ +function parseInvoice(invoiceString) { + const noteRegex = /tornadoInvoice-(?\w+)-(?[\d.]+)-(?\d+)-0x(?[0-9a-fA-F]{64})/g + const match = noteRegex.exec(invoiceString) + if (!match) { + throw new Error('The note has invalid format') + } + + const netId = Number(match.groups.netId) + const buf = Buffer.from(match.groups.commitmentNote, 'hex') + const commitmentNote = toHex(buf.slice(0, 32)) + + return { + currency: match.groups.currency, + amount: match.groups.amount, + netId, + commitmentNote + } +} + async function loadDepositData({ amount, currency, deposit }) { try { const cachedEvents = await fetchEvents({ type: 'deposit', currency, amount }); @@ -1257,6 +1323,27 @@ async function main() { .option('-R, --relayer ', 'Withdraw via relayer') .option('-T, --tor ', 'Optional tor port') .option('-L, --local', 'Local Node - Does not submit signed transaction to the node'); + program + .command('createNote ') + .description( + 'Create deposit note and invoice, allows generating private key like deposit notes from secure, offline environment. The currency is one of (ETH|DAI|cDAI|USDC|cUSDC|USDT). The amount depends on currency, see config.js file or visit https://tornado.cash.' + ) + .action(async (currency, amount, chainId) => { + currency = currency.toLowerCase(); + await createInvoice({ currency, amount, chainId }); + }); + program + .command('depositInvoice ') + .description( + 'Submit a deposit of invoice from default eth account and return the resulting note.' + ) + .action(async (invoice) => { + torPort = program.tor; + const { currency, amount, netId, commitmentNote } = parseInvoice(invoice); + await init({ rpc: program.rpc, currency, amount, localMode: program.local }); + console.log("Creating", currency.toUpperCase(), amount, "deposit for", netName, "Tornado Cash Instance"); + await deposit({ currency, amount, commitmentNote }); + }); program .command('deposit ') .description(