Improve EncryptedNotes

This commit is contained in:
Tornado Contrib 2024-10-26 18:54:31 +00:00
parent b9fa95f5be
commit 68f2e006ce
Signed by: tornadocontrib
GPG Key ID: 60B4DF1A076C64B1
24 changed files with 8159 additions and 5297 deletions

@ -60,6 +60,7 @@ module.exports = {
"error",
"always"
],
"@typescript-eslint/no-unused-vars": ["warn"]
"@typescript-eslint/no-unused-vars": ["warn"],
"@typescript-eslint/no-unused-expressions": ["off"]
}
}

10
.gitignore vendored

@ -1,7 +1,7 @@
node_modules
.env
/events
/trees
backup-tornado-*
backup-tornadoInvoice-*
backup-note-account-*
# coverage files
/coverage
/coverage.json
.nyc_output

13
.nycrc Normal file

@ -0,0 +1,13 @@
{
"all": true,
"extension": [".ts"],
"report-dir": "coverage",
"reporter": [
"html",
"lcov",
"text",
"text-summary"
],
"include": ["src/**/*.ts"],
"exclude": ["src/typechain/**/*"]
}

@ -1,6 +1,5 @@
import { EthEncryptedData } from '@metamask/eth-sig-util';
import { Echoer } from '@tornado/contracts';
import { Wallet } from 'ethers';
import { Signer, Wallet } from 'ethers';
import { EchoEvents, EncryptedNotesEvents } from './events';
import type { NetIdType } from './networkConfig';
export interface NoteToEncrypt {
@ -20,7 +19,6 @@ export interface NoteAccountConstructor {
netId: NetIdType;
blockNumber?: number;
recoveryKey?: string;
Echoer: Echoer;
}
export declare class NoteAccount {
netId: NetIdType;
@ -28,13 +26,12 @@ export declare class NoteAccount {
recoveryKey: string;
recoveryAddress: string;
recoveryPublicKey: string;
Echoer: Echoer;
constructor({ netId, blockNumber, recoveryKey, Echoer }: NoteAccountConstructor);
constructor({ netId, blockNumber, recoveryKey }: NoteAccountConstructor);
/**
* Intends to mock eth_getEncryptionPublicKey behavior from MetaMask
* In order to make the recoveryKey retrival from Echoer possible from the bare private key
*/
static getWalletPublicKey(wallet: Wallet): string;
static getSignerPublicKey(signer: Signer | Wallet): Promise<string>;
getEncryptedAccount(walletPublicKey: string): {
encryptedData: EthEncryptedData;
data: string;
@ -42,7 +39,7 @@ export declare class NoteAccount {
/**
* Decrypt Echoer backuped note encryption account with private keys
*/
decryptAccountsWithWallet(wallet: Wallet, events: EchoEvents[]): NoteAccount[];
decryptSignerNoteAccounts(signer: Signer | Wallet, events: EchoEvents[]): Promise<NoteAccount[]>;
decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[];
encryptNote({ address, noteHex }: NoteToEncrypt): string;
}

3
dist/hasher.d.ts vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/index.d.ts vendored

@ -8,6 +8,7 @@ export * from './encryptedNotes';
export * from './ens';
export * from './fees';
export * from './gaszip';
export * from './hasher';
export * from './idb';
export * from './ip';
export * from './merkleTree';

73
dist/index.js vendored

File diff suppressed because one or more lines are too long

72
dist/index.mjs vendored

File diff suppressed because one or more lines are too long

@ -102049,7 +102049,7 @@ function bytesToHex(bytes) {
}
function hexToBytes(hexString) {
if (hexString.slice(0, 2) === "0x") {
hexString = hexString.replace("0x", "");
hexString = hexString.slice(2);
}
if (hexString.length % 2 !== 0) {
hexString = "0" + hexString;
@ -102061,8 +102061,8 @@ function bytesToBN(bytes) {
}
function bnToBytes(bigint) {
let hexString = typeof bigint === "bigint" ? bigint.toString(16) : bigint;
if (hexString.startsWith("0x")) {
hexString = hexString.replace("0x", "");
if (hexString.slice(0, 2) === "0x") {
hexString = hexString.slice(2);
}
if (hexString.length % 2 !== 0) {
hexString = "0" + hexString;
@ -102085,6 +102085,9 @@ function toFixedLength(string, length = 32) {
function rBigInt(nbytes = 31) {
return bytesToBN(utils_crypto.getRandomValues(new Uint8Array(nbytes)));
}
function rHex(nbytes = 32) {
return bytesToHex(utils_crypto.getRandomValues(new Uint8Array(nbytes)));
}
function bigIntReplacer(key, value) {
return typeof value === "bigint" ? value.toString() : value;
}

10178
dist/tornado.umd.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/utils.d.ts vendored

@ -20,6 +20,7 @@ export declare function leInt2Buff(bigint: bnInput | bigint): Uint8Array;
export declare function toFixedHex(numberish: BigNumberish, length?: number): string;
export declare function toFixedLength(string: string, length?: number): string;
export declare function rBigInt(nbytes?: number): bigint;
export declare function rHex(nbytes?: number): string;
export declare function bigIntReplacer(key: any, value: any): any;
export declare function substring(str: string, length?: number): string;
export declare function digest(bytes: Uint8Array, algo?: string): Promise<Uint8Array>;

9
hardhat.config.ts Normal file

@ -0,0 +1,9 @@
import type { HardhatUserConfig } from 'hardhat/types';
import '@nomicfoundation/hardhat-toolbox';
import '@nomicfoundation/hardhat-ethers';
const config: HardhatUserConfig = {
solidity: '0.8.28',
};
export default config;

@ -9,11 +9,12 @@
"jsdelivr": "./dist/tornado.umd.min.js",
"scripts": {
"typechain": "typechain --target ethers-v6 --out-dir src/typechain src/abi/*.json",
"types": "tsc --declaration --emitDeclarationOnly",
"lint": "eslint src/**/*.ts --ext .ts --ignore-pattern src/typechain",
"types": "tsc --declaration --emitDeclarationOnly -p tsconfig.build.json",
"lint": "eslint src/**/*.ts test/**/*.ts --ext .ts --ignore-pattern src/typechain",
"build:node": "rollup -c",
"build:web": "webpack",
"build": "yarn types && yarn build:node && yarn build:web"
"build": "yarn types && yarn build:node && yarn build:web",
"test": "nyc mocha --require ts-node/register --require source-map-support/register --recursive 'test/**/*.ts' --timeout '300000'"
},
"author": "",
"license": "MIT",
@ -46,12 +47,23 @@
"idb": "^8.0.0"
},
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.7",
"@nomicfoundation/hardhat-ethers": "^3.0.8",
"@nomicfoundation/hardhat-ignition": "^0.15.5",
"@nomicfoundation/hardhat-ignition-ethers": "^0.15.5",
"@nomicfoundation/hardhat-network-helpers": "^1.0.11",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.10",
"@nomicfoundation/ignition-core": "^0.15.5",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@typechain/ethers-v6": "^0.5.1",
"@typechain/hardhat": "^9.1.0",
"@types/bn.js": "^5.1.6",
"@types/chai": "^4.2.0",
"@types/circomlibjs": "^0.1.6",
"@types/mocha": "^10.0.9",
"@types/node": "^22.8.0",
"@types/node-fetch": "^2.6.11",
"@typescript-eslint/eslint-plugin": "^8.11.0",
@ -62,10 +74,16 @@
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"hardhat": "^2.22.10",
"hardhat-gas-reporter": "^2.2.1",
"mocha": "^10.7.3",
"node-polyfill-webpack-plugin": "^4.0.0",
"nyc": "^17.1.0",
"prettier": "^3.3.3",
"rollup": "^4.24.0",
"rollup-plugin-esbuild": "^6.1.1",
"solidity-coverage": "^0.8.13",
"ts-node": "^10.9.2",
"tsc": "^2.0.4",
"typechain": "^8.3.2",
"typescript": "^5.6.3",

@ -1,7 +1,6 @@
import { getEncryptionPublicKey, encrypt, decrypt, EthEncryptedData } from '@metamask/eth-sig-util';
import { Echoer } from '@tornado/contracts';
import { Wallet, computeAddress, getAddress } from 'ethers';
import { crypto, base64ToBytes, bytesToBase64, bytesToHex, hexToBytes, toFixedHex, concatBytes } from './utils';
import { JsonRpcApiProvider, Signer, Wallet, computeAddress, getAddress } from 'ethers';
import { base64ToBytes, bytesToBase64, bytesToHex, hexToBytes, toFixedHex, concatBytes, rHex } from './utils';
import { EchoEvents, EncryptedNotesEvents } from './events';
import type { NetIdType } from './networkConfig';
@ -48,7 +47,6 @@ export interface NoteAccountConstructor {
blockNumber?: number;
// hex
recoveryKey?: string;
Echoer: Echoer;
}
export class NoteAccount {
@ -61,11 +59,10 @@ export class NoteAccount {
recoveryAddress: string;
// Note encryption public key derived from recoveryKey
recoveryPublicKey: string;
Echoer: Echoer;
constructor({ netId, blockNumber, recoveryKey, Echoer }: NoteAccountConstructor) {
constructor({ netId, blockNumber, recoveryKey }: NoteAccountConstructor) {
if (!recoveryKey) {
recoveryKey = bytesToHex(crypto.getRandomValues(new Uint8Array(32))).slice(2);
recoveryKey = rHex(32).slice(2);
}
this.netId = Math.floor(Number(netId));
@ -73,24 +70,28 @@ export class NoteAccount {
this.recoveryKey = recoveryKey;
this.recoveryAddress = computeAddress('0x' + recoveryKey);
this.recoveryPublicKey = getEncryptionPublicKey(recoveryKey);
this.Echoer = Echoer;
}
/**
* Intends to mock eth_getEncryptionPublicKey behavior from MetaMask
* In order to make the recoveryKey retrival from Echoer possible from the bare private key
*/
static getWalletPublicKey(wallet: Wallet) {
let { privateKey } = wallet;
if (privateKey.startsWith('0x')) {
privateKey = privateKey.replace('0x', '');
}
static async getSignerPublicKey(signer: Signer | Wallet) {
if ((signer as Wallet).privateKey) {
const wallet = signer as Wallet;
const privateKey = wallet.privateKey.slice(0, 2) === '0x' ? wallet.privateKey.slice(2) : wallet.privateKey;
// Should return base64 encoded public key
return getEncryptionPublicKey(privateKey);
}
const provider = signer.provider as JsonRpcApiProvider;
return (await provider.send('eth_getEncryptionPublicKey', [
(signer as Signer & { address: string }).address,
])) as string;
}
// This function intends to provide an encrypted value of recoveryKey for an on-chain Echoer backup purpose
// Thus, the pubKey should be derived by a Wallet instance or from Web3 wallets
// pubKey: base64 encoded 32 bytes key from https://docs.metamask.io/wallet/reference/eth_getencryptionpublickey/
@ -116,30 +117,53 @@ export class NoteAccount {
/**
* Decrypt Echoer backuped note encryption account with private keys
*/
decryptAccountsWithWallet(wallet: Wallet, events: EchoEvents[]): NoteAccount[] {
let { privateKey } = wallet;
if (privateKey.startsWith('0x')) {
privateKey = privateKey.replace('0x', '');
}
async decryptSignerNoteAccounts(signer: Signer | Wallet, events: EchoEvents[]): Promise<NoteAccount[]> {
const signerAddress = (signer as (Signer & { address: string }) | Wallet).address;
const decryptedEvents = [];
for (const event of events) {
if (event.address !== signerAddress) {
continue;
}
try {
const unpackedMessage = unpackEncryptedMessage(event.encryptedAccount);
const recoveryKey = decrypt({
let recoveryKey;
if ((signer as Wallet).privateKey) {
const wallet = signer as Wallet;
const privateKey = wallet.privateKey.slice(0, 2) === '0x' ? wallet.privateKey.slice(2) : wallet.privateKey;
recoveryKey = decrypt({
encryptedData: unpackedMessage,
privateKey,
});
} else {
const { version, nonce, ephemPublicKey, ciphertext } = unpackedMessage;
const unpackedBuffer = bytesToHex(
new TextEncoder().encode(
JSON.stringify({
version,
nonce,
ephemPublicKey,
ciphertext,
}),
),
);
const provider = signer.provider as JsonRpcApiProvider;
recoveryKey = await provider.send('eth_decrypt', [unpackedBuffer, signerAddress]);
}
decryptedEvents.push(
new NoteAccount({
netId: this.netId,
blockNumber: event.blockNumber,
recoveryKey,
Echoer: this.Echoer,
}),
);
} catch {

8
src/hasher.ts Normal file

File diff suppressed because one or more lines are too long

@ -8,6 +8,7 @@ export * from './encryptedNotes';
export * from './ens';
export * from './fees';
export * from './gaszip';
export * from './hasher';
export * from './idb';
export * from './ip';
export * from './merkleTree';

@ -586,7 +586,7 @@ export const defaultConfig: networkConfig = {
stablecoin: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
multicallContract: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0x1572AFE6949fdF51Cb3E0856216670ae9Ee160Ee',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
echoContract: '0xcDD1fc3F5ac2782D83449d3AbE80D6b7B273B0e5',
offchainOracleContract: '0x1f89EAF03E5b260Bc6D4Ae3c3334b1B750F3e127',
tornContract: '0x3AE6667167C0f44394106E197904519D808323cA',
governanceContract: '0xe5324cD7602eeb387418e594B87aCADee08aeCAD',

@ -72,7 +72,7 @@ export function bytesToHex(bytes: Uint8Array) {
export function hexToBytes(hexString: string) {
if (hexString.slice(0, 2) === '0x') {
hexString = hexString.replace('0x', '');
hexString = hexString.slice(2);
}
if (hexString.length % 2 !== 0) {
hexString = '0' + hexString;
@ -90,8 +90,8 @@ export function bnToBytes(bigint: bigint | string) {
// Parse bigint to hex string
let hexString: string = typeof bigint === 'bigint' ? bigint.toString(16) : bigint;
// Remove hex string prefix if exists
if (hexString.startsWith('0x')) {
hexString = hexString.replace('0x', '');
if (hexString.slice(0, 2) === '0x') {
hexString = hexString.slice(2);
}
// Hex string length should be a multiplier of two (To make correct bytes)
if (hexString.length % 2 !== 0) {
@ -130,6 +130,10 @@ export function rBigInt(nbytes: number = 31) {
return bytesToBN(crypto.getRandomValues(new Uint8Array(nbytes)));
}
export function rHex(nbytes: number = 32) {
return bytesToHex(crypto.getRandomValues(new Uint8Array(nbytes)));
}
// Used for JSON.stringify(value, bigIntReplacer, space)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function bigIntReplacer(key: any, value: any) {

58
test/deposit.ts Normal file

@ -0,0 +1,58 @@
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs';
import { ethers } from 'hardhat';
import { expect } from 'chai';
import { formatEther } from 'ethers';
import { ETHTornado__factory, Verifier__factory } from '@tornado/contracts';
import { Deposit, deployHasher } from '../src';
const { getSigners } = ethers;
const NOTES_COUNT = 100;
describe('./src/deposit.ts', function () {
const instanceFixture = async () => {
const [owner] = await getSigners();
const Hasher = (await (await deployHasher(owner)).wait())?.contractAddress as string;
const Verifier = await new Verifier__factory(owner).deploy();
const Instance = await new ETHTornado__factory(owner).deploy(Verifier.target, Hasher, 1n, 20);
return { Instance };
};
it('Deposit New Note', async function () {
const { Instance } = await loadFixture(instanceFixture);
const [owner] = await getSigners();
const netId = Number((await owner.provider.getNetwork()).chainId);
const deposit = await Deposit.createNote({ currency: 'eth', amount: formatEther(1), netId });
const resp = await Instance.deposit(deposit.commitmentHex, { value: 1n });
await expect(resp).to.emit(Instance, 'Deposit').withArgs(deposit.commitmentHex, 0, anyValue);
expect(await Instance.commitments(deposit.commitmentHex)).to.be.true;
});
xit(`Creating ${NOTES_COUNT} random notes`, async function () {
const notes = (await Promise.all(
// eslint-disable-next-line prefer-spread
Array.apply(null, Array(NOTES_COUNT)).map(() =>
Deposit.createNote({ currency: 'eth', amount: '0.1', netId: 31337 }),
),
)) as Deposit[];
notes.forEach(({ noteHex, commitmentHex, nullifierHex }) => {
// ((secret.length: 31) + (nullifier.length: 31)) * 2 + (prefix: 2) = 126
expect(noteHex.length === 126).to.be.true;
expect(commitmentHex.length === 66).to.be.true;
expect(nullifierHex.length === 66).to.be.true;
});
});
});

14
test/index.ts Normal file

@ -0,0 +1,14 @@
import { describe } from 'mocha';
import { ethers } from 'hardhat';
describe('Tornado Core', function () {
it('Get Provider', async function () {
const [owner] = await ethers.getSigners();
console.log(owner);
const { provider } = owner;
console.log(await provider.getBlock('latest'));
});
});

7
tsconfig.build.json Normal file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"files": []
}

@ -30,7 +30,7 @@
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"rootDir": ".", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
@ -110,5 +110,6 @@
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["./src/**/*"]
"include": ["./src/**/*"],
"files": ["./hardhat.config.ts"]
}

2776
yarn.lock

File diff suppressed because it is too large Load Diff