Save encrypted notes on router

This commit is contained in:
Tornado Contrib 2024-04-26 19:32:49 +00:00
parent 195da66ce2
commit 3312f44a4d
Signed by: tornadocontrib
GPG Key ID: 60B4DF1A076C64B1
7 changed files with 180 additions and 123 deletions

@ -42,10 +42,12 @@ module.exports = {
} }
], ],
"import/order": ["error"], "import/order": ["error"],
/**
"indent": [ "indent": [
"error", "error",
2 2
], ],
**/
"linebreak-style": [ "linebreak-style": [
"error", "error",
"unix" "unix"

@ -26,7 +26,7 @@
"compliance": "ts-node src/cli.ts compliance", "compliance": "ts-node src/cli.ts compliance",
"syncEvents": "ts-node src/cli.ts syncEvents", "syncEvents": "ts-node src/cli.ts syncEvents",
"relayers": "ts-node src/cli.ts relayers", "relayers": "ts-node src/cli.ts relayers",
"createNoteAccount": "ts-node src/cli.ts createNoteAccount", "createAccount": "ts-node src/cli.ts createAccount",
"decryptNotes": "ts-node src/cli.ts decryptNotes", "decryptNotes": "ts-node src/cli.ts decryptNotes",
"send": "ts-node src/cli.ts send", "send": "ts-node src/cli.ts send",
"balance": "ts-node src/cli.ts balance", "balance": "ts-node src/cli.ts balance",

@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any, prettier/prettier */ /* eslint-disable @typescript-eslint/no-explicit-any */
import workerThreads from 'worker_threads'; import workerThreads from 'worker_threads';
import { MerkleTree, Element, TreeEdge, PartialMerkleTree } from '@tornado/fixed-merkle-tree'; import { MerkleTree, Element, TreeEdge, PartialMerkleTree } from '@tornado/fixed-merkle-tree';
import { mimc, isNode } from './services'; import { mimc, isNode } from './services';
@ -65,5 +65,5 @@ if (isNode && workerThreads) {
postMessage(merkleTree.toString()); postMessage(merkleTree.toString());
}); });
} else { } else {
throw new Error('This browser / environment doesn\'t support workers!'); throw new Error('This browser / environment does not support workers!');
} }

@ -524,13 +524,14 @@ export function tornadoProgram() {
.action(async (netId: string | number, currency: string, amount: string, cmdOptions: commonProgramOptions) => { .action(async (netId: string | number, currency: string, amount: string, cmdOptions: commonProgramOptions) => {
const { options, fetchDataOptions } = await getProgramOptions(cmdOptions); const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
currency = currency.toLowerCase(); currency = currency.toLowerCase();
const { rpc } = options; const { rpc, accountKey } = options;
const config = networkConfig[`netId${netId}`]; const config = networkConfig[`netId${netId}`];
const { const {
multicall: multicallAddress, multicall: multicallAddress,
routerContract, routerContract,
echoContract,
nativeCurrency, nativeCurrency,
tokens: { [currency]: currencyConfig }, tokens: { [currency]: currencyConfig },
} = config; } = config;
@ -553,6 +554,14 @@ export function tornadoProgram() {
provider, provider,
}); });
const noteAccount = accountKey
? new NoteAccount({
netId,
recoveryKey: accountKey,
Echoer: Echoer__factory.connect(echoContract, provider),
})
: undefined;
if (!signer) { if (!signer) {
throw new Error( throw new Error(
'Signer not defined, make sure you have either viewOnly address, mnemonic, or private key configured', 'Signer not defined, make sure you have either viewOnly address, mnemonic, or private key configured',
@ -569,7 +578,6 @@ export function tornadoProgram() {
name: 'getEthBalance', name: 'getEthBalance',
params: [signer.address], params: [signer.address],
}, },
/* eslint-disable prettier/prettier */
...(!isEth ...(!isEth
? [ ? [
{ {
@ -584,7 +592,6 @@ export function tornadoProgram() {
}, },
] ]
: []), : []),
/* eslint-enable prettier/prettier */
]); ]);
if (isEth && denomination > ethBalance) { if (isEth && denomination > ethBalance) {
@ -616,18 +623,36 @@ export function tornadoProgram() {
const { note, noteHex, commitmentHex } = deposit; const { note, noteHex, commitmentHex } = deposit;
const encryptedNote = noteAccount
? noteAccount.encryptNote({
address: instanceAddress,
noteHex,
})
: '0x';
const backupFile = `./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`;
console.log(`New deposit: ${deposit.toString()}\n`); console.log(`New deposit: ${deposit.toString()}\n`);
await writeFile(`./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`, note, { console.log(`Writing note backup at ${backupFile}\n`);
encoding: 'utf8',
}); await writeFile(backupFile, note, { encoding: 'utf8' });
if (encryptedNote !== '0x') {
console.log(`Storing encrypted note on-chain for backup (Account key: ${accountKey})\n`);
}
await programSendTransaction({ await programSendTransaction({
signer, signer,
options, options,
populatedTransaction: await TornadoProxy.deposit.populateTransaction(instanceAddress, commitmentHex, '0x', { populatedTransaction: await TornadoProxy.deposit.populateTransaction(
instanceAddress,
commitmentHex,
encryptedNote,
{
value: isEth ? denomination : BigInt(0), value: isEth ? denomination : BigInt(0),
}), },
),
}); });
process.exit(0); process.exit(0);
@ -689,7 +714,6 @@ export function tornadoProgram() {
name: 'getEthBalance', name: 'getEthBalance',
params: [signer.address], params: [signer.address],
}, },
/* eslint-disable prettier/prettier */
...(!isEth ...(!isEth
? [ ? [
{ {
@ -704,7 +728,6 @@ export function tornadoProgram() {
}, },
] ]
: []), : []),
/* eslint-enable prettier/prettier */
]); ]);
if (isEth && denomination > ethBalance) { if (isEth && denomination > ethBalance) {
@ -923,7 +946,6 @@ export function tornadoProgram() {
readFile(CIRCUIT_PATH, { encoding: 'utf8' }).then((s) => JSON.parse(s)), readFile(CIRCUIT_PATH, { encoding: 'utf8' }).then((s) => JSON.parse(s)),
readFile(KEY_PATH).then((b) => new Uint8Array(b).buffer), readFile(KEY_PATH).then((b) => new Uint8Array(b).buffer),
depositTreePromise, depositTreePromise,
/* eslint-disable prettier/prettier */
!walletWithdrawal !walletWithdrawal
? getProgramRelayer({ ? getProgramRelayer({
options, options,
@ -931,7 +953,6 @@ export function tornadoProgram() {
netId, netId,
}).then(({ relayerClient }) => relayerClient) }).then(({ relayerClient }) => relayerClient)
: undefined, : undefined,
/* eslint-enable prettier/prettier */
tornadoFeeOracle.fetchL1OptimismFee(), tornadoFeeOracle.fetchL1OptimismFee(),
!isEth ? tokenPriceOracle.fetchPrices([tokenAddress as string]).then((p) => p[0]) : BigInt(0), !isEth ? tokenPriceOracle.fetchPrices([tokenAddress as string]).then((p) => p[0]) : BigInt(0),
provider.getFeeData(), provider.getFeeData(),
@ -1466,7 +1487,7 @@ export function tornadoProgram() {
}); });
program program
.command('createNoteAccount') .command('createAccount')
.description( .description(
'Creates and save on-chain account that would store encrypted notes. \n\n' + 'Creates and save on-chain account that would store encrypted notes. \n\n' +
'Would first lookup on on-chain records to see if the notes are stored. \n\n' + 'Would first lookup on on-chain records to see if the notes are stored. \n\n' +
@ -1532,7 +1553,7 @@ export function tornadoProgram() {
const userEvents = echoEvents.filter(({ address }) => address === signer.address); const userEvents = echoEvents.filter(({ address }) => address === signer.address);
const existingAccounts = userEvents.map((e) => newAccount.decryptAccountWithWallet(signer, e)); const existingAccounts = newAccount.decryptAccountsWithWallet(signer, userEvents);
const accountsTable = new Table(); const accountsTable = new Table();
@ -1587,7 +1608,7 @@ export function tornadoProgram() {
.argument('<netId>', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber) .argument('<netId>', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
.argument( .argument(
'[accountKey]', '[accountKey]',
'Account key generated from UI or the createNoteAccount to store encrypted notes on-chain', 'Account key generated from UI or the createAccount to store encrypted notes on-chain',
parseRecoveryKey, parseRecoveryKey,
) )
.action(async (netId: string | number, accountKey: string | undefined, cmdOptions: commonProgramOptions) => { .action(async (netId: string | number, accountKey: string | undefined, cmdOptions: commonProgramOptions) => {
@ -1614,7 +1635,7 @@ export function tornadoProgram() {
if (!accountKey) { if (!accountKey) {
throw new Error( throw new Error(
'No account key find! Please supply correct account key from either UI or find one with createNoteAccount command', 'No account key find! Please supply correct account key from either UI or find one with createAccount command',
); );
} }
@ -1720,7 +1741,6 @@ export function tornadoProgram() {
const txGasPrice = feeData.maxFeePerGas const txGasPrice = feeData.maxFeePerGas
? feeData.maxFeePerGas + (feeData.maxPriorityFeePerGas || BigInt(0)) ? feeData.maxFeePerGas + (feeData.maxPriorityFeePerGas || BigInt(0))
: feeData.gasPrice || BigInt(0); : feeData.gasPrice || BigInt(0);
/* eslint-disable prettier/prettier */
const txFees = feeData.maxFeePerGas const txFees = feeData.maxFeePerGas
? { ? {
maxFeePerGas: feeData.maxFeePerGas, maxFeePerGas: feeData.maxFeePerGas,
@ -1764,10 +1784,10 @@ export function tornadoProgram() {
...txFees, ...txFees,
}); });
const bumpedGas = (estimatedGas !== BigInt(21000) && signer.gasLimitBump const bumpedGas =
estimatedGas !== BigInt(21000) && signer.gasLimitBump
? (estimatedGas * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000) ? (estimatedGas * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000)
: estimatedGas : estimatedGas;
);
toSend = ethBalance - txGasPrice * bumpedGas; toSend = ethBalance - txGasPrice * bumpedGas;
} }
@ -1787,7 +1807,6 @@ export function tornadoProgram() {
...txFees, ...txFees,
}), }),
}); });
/* eslint-enable prettier/prettier */
process.exit(0); process.exit(0);
}, },
@ -1891,7 +1910,6 @@ export function tornadoProgram() {
options, options,
populatedTransaction: deserializedTx, populatedTransaction: deserializedTx,
}); });
/* eslint-enable prettier/prettier */
process.exit(0); process.exit(0);
}); });
@ -1924,14 +1942,24 @@ export function tornadoProgram() {
}); });
// common options // common options
/* eslint-disable prettier/prettier */
program.commands.forEach((cmd) => { program.commands.forEach((cmd) => {
cmd.option('-r, --rpc <RPC_URL>', 'The RPC that CLI should interact with', parseUrl); cmd.option('-r, --rpc <RPC_URL>', 'The RPC that CLI should interact with', parseUrl);
cmd.option('-e, --eth-rpc <ETHRPC_URL>', 'The Ethereum Mainnet RPC that CLI should interact with', parseUrl); cmd.option('-e, --eth-rpc <ETHRPC_URL>', 'The Ethereum Mainnet RPC that CLI should interact with', parseUrl);
cmd.option('-g, --graph <GRAPH_URL>', 'The Subgraph API that CLI should interact with', parseUrl); cmd.option('-g, --graph <GRAPH_URL>', 'The Subgraph API that CLI should interact with', parseUrl);
cmd.option('-G, --eth-graph <ETHGRAPH_URL>', 'The Ethereum Mainnet Subgraph API that CLI should interact with', parseUrl); cmd.option(
cmd.option('-d, --disable-graph', 'Disable Graph API - Does not enable Subgraph API and use only local RPC as an event source'); '-G, --eth-graph <ETHGRAPH_URL>',
cmd.option('-a, --account-key <ACCOUNT_KEY>', 'Account key generated from UI or the createNoteAccount to store encrypted notes on-chain', parseRecoveryKey); 'The Ethereum Mainnet Subgraph API that CLI should interact with',
parseUrl,
);
cmd.option(
'-d, --disable-graph',
'Disable Graph API - Does not enable Subgraph API and use only local RPC as an event source',
);
cmd.option(
'-a, --account-key <ACCOUNT_KEY>',
'Account key generated from UI or the createAccount to store encrypted notes on-chain',
parseRecoveryKey,
);
cmd.option('-R, --relayer <RELAYER>', 'Withdraw via relayer (Should be either .eth name or URL)', parseRelayer); cmd.option('-R, --relayer <RELAYER>', 'Withdraw via relayer (Should be either .eth name or URL)', parseRelayer);
cmd.option('-w, --wallet-withdrawal', 'Withdrawal via wallet (Should not be linked with deposits)'); cmd.option('-w, --wallet-withdrawal', 'Withdrawal via wallet (Should not be linked with deposits)');
cmd.option('-T, --tor-port <TOR_PORT>', 'Optional tor port', parseNumber); cmd.option('-T, --tor-port <TOR_PORT>', 'Optional tor port', parseNumber);
@ -1943,13 +1971,13 @@ export function tornadoProgram() {
); );
cmd.option( cmd.option(
'-m, --mnemonic <MNEMONIC>', '-m, --mnemonic <MNEMONIC>',
'Wallet BIP39 Mnemonic Phrase - If you didn\'t add it to .env file and it is needed for operation', 'Wallet BIP39 Mnemonic Phrase - If you did not add it to .env file and it is needed for operation',
parseMnemonic, parseMnemonic,
); );
cmd.option('-i, --mnemonic-index <MNEMONIC_INDEX>', 'Optional wallet mnemonic index', parseNumber); cmd.option('-i, --mnemonic-index <MNEMONIC_INDEX>', 'Optional wallet mnemonic index', parseNumber);
cmd.option( cmd.option(
'-p, --private-key <PRIVATE_KEY>', '-p, --private-key <PRIVATE_KEY>',
'Wallet private key - If you didn\'t add it to .env file and it is needed for operation', 'Wallet private key - If you did not add it to .env file and it is needed for operation',
parseKey, parseKey,
); );
cmd.option( cmd.option(
@ -1958,7 +1986,6 @@ export function tornadoProgram() {
); );
cmd.option('-l, --local-rpc', 'Local node mode - Does not submit signed transaction to the node'); cmd.option('-l, --local-rpc', 'Local node mode - Does not submit signed transaction to the node');
}); });
/* eslint-enable prettier/prettier */
return program; return program;
} }

@ -4,6 +4,11 @@ import { Wallet, computeAddress } from 'ethers';
import { crypto, base64ToBytes, bytesToBase64, bytesToHex, hexToBytes, toFixedHex, concatBytes } from './utils'; import { crypto, base64ToBytes, bytesToBase64, bytesToHex, hexToBytes, toFixedHex, concatBytes } from './utils';
import { EchoEvents, EncryptedNotesEvents } from './events'; import { EchoEvents, EncryptedNotesEvents } from './events';
export interface NoteToEncrypt {
address: string;
noteHex: string;
}
export interface DecryptedNotes { export interface DecryptedNotes {
blockNumber: number; blockNumber: number;
address: string; address: string;
@ -110,13 +115,17 @@ export class NoteAccount {
/** /**
* Decrypt Echoer backuped note encryption account with private keys * Decrypt Echoer backuped note encryption account with private keys
*/ */
decryptAccountWithWallet(wallet: Wallet, event: EchoEvents): NoteAccount { decryptAccountsWithWallet(wallet: Wallet, events: EchoEvents[]): NoteAccount[] {
let { privateKey } = wallet; let { privateKey } = wallet;
if (privateKey.startsWith('0x')) { if (privateKey.startsWith('0x')) {
privateKey = privateKey.replace('0x', ''); privateKey = privateKey.replace('0x', '');
} }
const decryptedEvents = [];
for (const event of events) {
try {
const unpackedMessage = unpackEncryptedMessage(event.encryptedAccount); const unpackedMessage = unpackEncryptedMessage(event.encryptedAccount);
const recoveryKey = decrypt({ const recoveryKey = decrypt({
@ -124,12 +133,21 @@ export class NoteAccount {
privateKey, privateKey,
}); });
return new NoteAccount({ decryptedEvents.push(
new NoteAccount({
netId: this.netId, netId: this.netId,
blockNumber: event.blockNumber, blockNumber: event.blockNumber,
recoveryKey, recoveryKey,
Echoer: this.Echoer, Echoer: this.Echoer,
}); }),
);
} catch {
// decryption may fail for invalid accounts
continue;
}
}
return decryptedEvents;
} }
decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[] { decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[] {
@ -157,4 +175,14 @@ export class NoteAccount {
return decryptedEvents; return decryptedEvents;
} }
encryptNote({ address, noteHex }: NoteToEncrypt) {
const encryptedData = encrypt({
publicKey: this.recoveryPublicKey,
data: `${address}-${noteHex}`,
version: 'x25519-xsalsa20-poly1305',
});
return packEncryptedMessage(encryptedData);
}
} }

@ -8,7 +8,7 @@ import type { DepositsEvents } from './events';
export type MerkleTreeConstructor = DepositType & { export type MerkleTreeConstructor = DepositType & {
Tornado: Tornado; Tornado: Tornado;
commitment?: string; commitmentHex?: string;
merkleTreeHeight?: number; merkleTreeHeight?: number;
emptyElement?: string; emptyElement?: string;
merkleWorkerPath?: string; merkleWorkerPath?: string;
@ -19,7 +19,7 @@ export class MerkleTreeService {
amount: string; amount: string;
netId: number; netId: number;
Tornado: Tornado; Tornado: Tornado;
commitment?: string; commitmentHex?: string;
instanceName: string; instanceName: string;
merkleTreeHeight: number; merkleTreeHeight: number;
@ -32,7 +32,7 @@ export class MerkleTreeService {
amount, amount,
currency, currency,
Tornado, Tornado,
commitment, commitmentHex,
merkleTreeHeight = 20, merkleTreeHeight = 20,
emptyElement = '21663839004416932945382355908790599225266501822907911457504978515578255421292', emptyElement = '21663839004416932945382355908790599225266501822907911457504978515578255421292',
merkleWorkerPath, merkleWorkerPath,
@ -45,7 +45,7 @@ export class MerkleTreeService {
this.Tornado = Tornado; this.Tornado = Tornado;
this.instanceName = instanceName; this.instanceName = instanceName;
this.commitment = commitment; this.commitmentHex = commitmentHex;
this.merkleTreeHeight = merkleTreeHeight; this.merkleTreeHeight = merkleTreeHeight;
this.emptyElement = emptyElement; this.emptyElement = emptyElement;

@ -213,7 +213,7 @@ export async function fetchData(url: string, options: fetchDataOptions = {}) {
throw errorObject; throw errorObject;
} }
/* eslint-disable prettier/prettier, @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
export const fetchGetUrlFunc = export const fetchGetUrlFunc =
(options: fetchDataOptions = {}): FetchGetUrlFunc => (options: fetchDataOptions = {}): FetchGetUrlFunc =>
async (req, _signal) => { async (req, _signal) => {
@ -253,7 +253,7 @@ export const fetchGetUrlFunc =
body, body,
}; };
}; };
/* eslint-enable prettier/prettier, @typescript-eslint/no-explicit-any */ /* eslint-enable @typescript-eslint/no-explicit-any */
// caching to improve performance // caching to improve performance
const oracleMapper = new Map(); const oracleMapper = new Map();