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"],
/**
"indent": [
"error",
2
],
**/
"linebreak-style": [
"error",
"unix"

@ -26,7 +26,7 @@
"compliance": "ts-node src/cli.ts compliance",
"syncEvents": "ts-node src/cli.ts syncEvents",
"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",
"send": "ts-node src/cli.ts send",
"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 { MerkleTree, Element, TreeEdge, PartialMerkleTree } from '@tornado/fixed-merkle-tree';
import { mimc, isNode } from './services';
@ -65,5 +65,5 @@ if (isNode && workerThreads) {
postMessage(merkleTree.toString());
});
} 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) => {
const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
currency = currency.toLowerCase();
const { rpc } = options;
const { rpc, accountKey } = options;
const config = networkConfig[`netId${netId}`];
const {
multicall: multicallAddress,
routerContract,
echoContract,
nativeCurrency,
tokens: { [currency]: currencyConfig },
} = config;
@ -553,6 +554,14 @@ export function tornadoProgram() {
provider,
});
const noteAccount = accountKey
? new NoteAccount({
netId,
recoveryKey: accountKey,
Echoer: Echoer__factory.connect(echoContract, provider),
})
: undefined;
if (!signer) {
throw new Error(
'Signer not defined, make sure you have either viewOnly address, mnemonic, or private key configured',
@ -569,22 +578,20 @@ export function tornadoProgram() {
name: 'getEthBalance',
params: [signer.address],
},
/* eslint-disable prettier/prettier */
...(!isEth
? [
{
contract: Token as ERC20,
name: 'balanceOf',
params: [signer.address],
},
{
contract: Token as ERC20,
name: 'allowance',
params: [signer.address, routerContract],
},
]
{
contract: Token as ERC20,
name: 'balanceOf',
params: [signer.address],
},
{
contract: Token as ERC20,
name: 'allowance',
params: [signer.address, routerContract],
},
]
: []),
/* eslint-enable prettier/prettier */
]);
if (isEth && denomination > ethBalance) {
@ -616,18 +623,36 @@ export function tornadoProgram() {
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`);
await writeFile(`./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`, note, {
encoding: 'utf8',
});
console.log(`Writing note backup at ${backupFile}\n`);
await writeFile(backupFile, note, { encoding: 'utf8' });
if (encryptedNote !== '0x') {
console.log(`Storing encrypted note on-chain for backup (Account key: ${accountKey})\n`);
}
await programSendTransaction({
signer,
options,
populatedTransaction: await TornadoProxy.deposit.populateTransaction(instanceAddress, commitmentHex, '0x', {
value: isEth ? denomination : BigInt(0),
}),
populatedTransaction: await TornadoProxy.deposit.populateTransaction(
instanceAddress,
commitmentHex,
encryptedNote,
{
value: isEth ? denomination : BigInt(0),
},
),
});
process.exit(0);
@ -689,22 +714,20 @@ export function tornadoProgram() {
name: 'getEthBalance',
params: [signer.address],
},
/* eslint-disable prettier/prettier */
...(!isEth
? [
{
contract: Token as ERC20,
name: 'balanceOf',
params: [signer.address],
},
{
contract: Token as ERC20,
name: 'allowance',
params: [signer.address, routerContract],
},
]
{
contract: Token as ERC20,
name: 'balanceOf',
params: [signer.address],
},
{
contract: Token as ERC20,
name: 'allowance',
params: [signer.address, routerContract],
},
]
: []),
/* eslint-enable prettier/prettier */
]);
if (isEth && denomination > ethBalance) {
@ -923,15 +946,13 @@ export function tornadoProgram() {
readFile(CIRCUIT_PATH, { encoding: 'utf8' }).then((s) => JSON.parse(s)),
readFile(KEY_PATH).then((b) => new Uint8Array(b).buffer),
depositTreePromise,
/* eslint-disable prettier/prettier */
!walletWithdrawal
? getProgramRelayer({
options,
fetchDataOptions,
netId,
}).then(({ relayerClient }) => relayerClient)
options,
fetchDataOptions,
netId,
}).then(({ relayerClient }) => relayerClient)
: undefined,
/* eslint-enable prettier/prettier */
tornadoFeeOracle.fetchL1OptimismFee(),
!isEth ? tokenPriceOracle.fetchPrices([tokenAddress as string]).then((p) => p[0]) : BigInt(0),
provider.getFeeData(),
@ -1466,7 +1487,7 @@ export function tornadoProgram() {
});
program
.command('createNoteAccount')
.command('createAccount')
.description(
'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' +
@ -1532,7 +1553,7 @@ export function tornadoProgram() {
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();
@ -1587,7 +1608,7 @@ export function tornadoProgram() {
.argument('<netId>', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
.argument(
'[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,
)
.action(async (netId: string | number, accountKey: string | undefined, cmdOptions: commonProgramOptions) => {
@ -1614,7 +1635,7 @@ export function tornadoProgram() {
if (!accountKey) {
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,15 +1741,14 @@ export function tornadoProgram() {
const txGasPrice = feeData.maxFeePerGas
? feeData.maxFeePerGas + (feeData.maxPriorityFeePerGas || BigInt(0))
: feeData.gasPrice || BigInt(0);
/* eslint-disable prettier/prettier */
const txFees = feeData.maxFeePerGas
? {
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
}
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
}
: {
gasPrice: feeData.gasPrice,
};
gasPrice: feeData.gasPrice,
};
let toSend: bigint;
@ -1764,10 +1784,10 @@ export function tornadoProgram() {
...txFees,
});
const bumpedGas = (estimatedGas !== BigInt(21000) && signer.gasLimitBump
? (estimatedGas * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000)
: estimatedGas
);
const bumpedGas =
estimatedGas !== BigInt(21000) && signer.gasLimitBump
? (estimatedGas * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000)
: estimatedGas;
toSend = ethBalance - txGasPrice * bumpedGas;
}
@ -1779,15 +1799,14 @@ export function tornadoProgram() {
populatedTransaction: tokenAddress
? await Token.transfer.populateTransaction(to, toSend)
: await signer.populateTransaction({
type: txType,
from: signer.address,
to,
value: toSend,
nonce,
...txFees,
}),
type: txType,
from: signer.address,
to,
value: toSend,
nonce,
...txFees,
}),
});
/* eslint-enable prettier/prettier */
process.exit(0);
},
@ -1891,7 +1910,6 @@ export function tornadoProgram() {
options,
populatedTransaction: deserializedTx,
});
/* eslint-enable prettier/prettier */
process.exit(0);
});
@ -1924,14 +1942,24 @@ export function tornadoProgram() {
});
// common options
/* eslint-disable prettier/prettier */
program.commands.forEach((cmd) => {
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('-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('-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 createNoteAccount to store encrypted notes on-chain', parseRecoveryKey);
cmd.option(
'-G, --eth-graph <ETHGRAPH_URL>',
'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('-w, --wallet-withdrawal', 'Withdrawal via wallet (Should not be linked with deposits)');
cmd.option('-T, --tor-port <TOR_PORT>', 'Optional tor port', parseNumber);
@ -1943,13 +1971,13 @@ export function tornadoProgram() {
);
cmd.option(
'-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,
);
cmd.option('-i, --mnemonic-index <MNEMONIC_INDEX>', 'Optional wallet mnemonic index', parseNumber);
cmd.option(
'-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,
);
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');
});
/* eslint-enable prettier/prettier */
return program;
}

@ -4,6 +4,11 @@ import { Wallet, computeAddress } from 'ethers';
import { crypto, base64ToBytes, bytesToBase64, bytesToHex, hexToBytes, toFixedHex, concatBytes } from './utils';
import { EchoEvents, EncryptedNotesEvents } from './events';
export interface NoteToEncrypt {
address: string;
noteHex: string;
}
export interface DecryptedNotes {
blockNumber: number;
address: string;
@ -110,26 +115,39 @@ export class NoteAccount {
/**
* Decrypt Echoer backuped note encryption account with private keys
*/
decryptAccountWithWallet(wallet: Wallet, event: EchoEvents): NoteAccount {
decryptAccountsWithWallet(wallet: Wallet, events: EchoEvents[]): NoteAccount[] {
let { privateKey } = wallet;
if (privateKey.startsWith('0x')) {
privateKey = privateKey.replace('0x', '');
}
const unpackedMessage = unpackEncryptedMessage(event.encryptedAccount);
const decryptedEvents = [];
const recoveryKey = decrypt({
encryptedData: unpackedMessage,
privateKey,
});
for (const event of events) {
try {
const unpackedMessage = unpackEncryptedMessage(event.encryptedAccount);
return new NoteAccount({
netId: this.netId,
blockNumber: event.blockNumber,
recoveryKey,
Echoer: this.Echoer,
});
const recoveryKey = decrypt({
encryptedData: unpackedMessage,
privateKey,
});
decryptedEvents.push(
new NoteAccount({
netId: this.netId,
blockNumber: event.blockNumber,
recoveryKey,
Echoer: this.Echoer,
}),
);
} catch {
// decryption may fail for invalid accounts
continue;
}
}
return decryptedEvents;
}
decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[] {
@ -157,4 +175,14 @@ export class NoteAccount {
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 & {
Tornado: Tornado;
commitment?: string;
commitmentHex?: string;
merkleTreeHeight?: number;
emptyElement?: string;
merkleWorkerPath?: string;
@ -19,7 +19,7 @@ export class MerkleTreeService {
amount: string;
netId: number;
Tornado: Tornado;
commitment?: string;
commitmentHex?: string;
instanceName: string;
merkleTreeHeight: number;
@ -32,7 +32,7 @@ export class MerkleTreeService {
amount,
currency,
Tornado,
commitment,
commitmentHex,
merkleTreeHeight = 20,
emptyElement = '21663839004416932945382355908790599225266501822907911457504978515578255421292',
merkleWorkerPath,
@ -45,7 +45,7 @@ export class MerkleTreeService {
this.Tornado = Tornado;
this.instanceName = instanceName;
this.commitment = commitment;
this.commitmentHex = commitmentHex;
this.merkleTreeHeight = merkleTreeHeight;
this.emptyElement = emptyElement;

@ -213,47 +213,47 @@ export async function fetchData(url: string, options: fetchDataOptions = {}) {
throw errorObject;
}
/* eslint-disable prettier/prettier, @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-explicit-any */
export const fetchGetUrlFunc =
(options: fetchDataOptions = {}): FetchGetUrlFunc =>
async (req, _signal) => {
let signal;
async (req, _signal) => {
let signal;
if (_signal) {
const controller = new AbortController();
signal = controller.signal;
_signal.addListener(() => {
controller.abort();
});
}
const init = {
...options,
method: req.method || 'POST',
headers: req.headers,
body: req.body || undefined,
signal,
returnResponse: true,
};
const resp = await fetchData(req.url, init);
const headers = {} as { [key in string]: any };
resp.headers.forEach((value: any, key: string) => {
headers[key.toLowerCase()] = value;
if (_signal) {
const controller = new AbortController();
signal = controller.signal;
_signal.addListener(() => {
controller.abort();
});
}
const respBody = await resp.arrayBuffer();
const body = respBody == null ? null : new Uint8Array(respBody);
return {
statusCode: resp.status,
statusMessage: resp.statusText,
headers,
body,
};
const init = {
...options,
method: req.method || 'POST',
headers: req.headers,
body: req.body || undefined,
signal,
returnResponse: true,
};
/* eslint-enable prettier/prettier, @typescript-eslint/no-explicit-any */
const resp = await fetchData(req.url, init);
const headers = {} as { [key in string]: any };
resp.headers.forEach((value: any, key: string) => {
headers[key.toLowerCase()] = value;
});
const respBody = await resp.arrayBuffer();
const body = respBody == null ? null : new Uint8Array(respBody);
return {
statusCode: resp.status,
statusMessage: resp.statusText,
headers,
body,
};
};
/* eslint-enable @typescript-eslint/no-explicit-any */
// caching to improve performance
const oracleMapper = new Map();