diff --git a/config/default.js b/config/default.js index 801dd1991..d5c648d52 100644 --- a/config/default.js +++ b/config/default.js @@ -6,7 +6,7 @@ function configureAWSBucket() { } function parseCircuitFilesPath() { - let circuits = ['deposit', 'withdraw', 'single_transfer', 'double_transfer']; + let circuits = ['deposit', 'transfer', 'withdraw']; if (process.env.USE_STUBS === 'true') circuits = circuits.map(circuit => `${circuit}_stub`); const parsedPath = {}; for (const circuit of circuits) { @@ -103,7 +103,7 @@ module.exports = { TRANSACTIONS_PER_BLOCK: Number(process.env.TRANSACTIONS_PER_BLOCK) || 2, RETRIES: Number(process.env.AUTOSTART_RETRIES) || 50, USE_STUBS: process.env.USE_STUBS === 'true', - VK_IDS: { deposit: 0, transfer: 1, withdraw: 2 }, // withdraw: 3, withdraw_change: 4 }, // used as an enum to mirror the Shield contracts enum for vk types. The keys of this object must correspond to a 'folderpath' (the .zok file without the '.zok' bit) + VK_IDS: { deposit: 0, transfer: 1, withdraw: 2 }, // used as an enum to mirror the Shield contracts enum for vk types. The keys of this object must correspond to a 'folderpath' (the .zok file without the '.zok' bit) MAX_PUBLIC_VALUES: { ERCADDRESS: 2n ** 161n - 1n, COMMITMENT: 2n ** 249n - 1n, diff --git a/nightfall-client/src/services/commitment-sync.mjs b/nightfall-client/src/services/commitment-sync.mjs index 549cab2b1..b08c9d558 100644 --- a/nightfall-client/src/services/commitment-sync.mjs +++ b/nightfall-client/src/services/commitment-sync.mjs @@ -53,9 +53,6 @@ export async function decryptCommitment(transaction, zkpPrivateKey, nullifierKey } }); - if (storeCommitments.length === 0) { - throw Error("This encrypted message isn't for any of recipients"); - } return Promise.all(storeCommitments); } diff --git a/wallet/package-lock.json b/wallet/package-lock.json index ccc967a6f..024e086d3 100644 --- a/wallet/package-lock.json +++ b/wallet/package-lock.json @@ -1918,82 +1918,6 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, - "@chainlink/contracts-0.0.10": { - "version": "npm:@chainlink/contracts@0.0.10", - "resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.0.10.tgz", - "integrity": "sha512-ok+ucSQ+3mrR+zjbi6zIrdd5M9XymcqVcnXGVyqBVRYZp97jS2/rt/glP320JmHxmi4pacgDOg0Ux11xIr1S8Q==", - "dev": true, - "requires": { - "@truffle/contract": "^4.2.6", - "ethers": "^4.0.45" - }, - "dependencies": { - "aes-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", - "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", - "dev": true, - "optional": true - }, - "ethers": { - "version": "4.0.49", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.49.tgz", - "integrity": "sha512-kPltTvWiyu+OktYy1IStSO16i2e7cS9D9OxZ81q2UUaiNPVrm/RTcbxamCXF9VUSKzJIdJV68EAIhTEVBalRWg==", - "dev": true, - "optional": true, - "requires": { - "aes-js": "3.0.0", - "bn.js": "^4.11.9", - "elliptic": "6.5.4", - "hash.js": "1.1.3", - "js-sha3": "0.5.7", - "scrypt-js": "2.0.4", - "setimmediate": "1.0.4", - "uuid": "2.0.1", - "xmlhttprequest": "1.8.0" - } - }, - "hash.js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", - "dev": true, - "optional": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.0" - } - }, - "js-sha3": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", - "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=", - "dev": true, - "optional": true - }, - "scrypt-js": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.4.tgz", - "integrity": "sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw==", - "dev": true, - "optional": true - }, - "setimmediate": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz", - "integrity": "sha1-IOgd5iLUoCWIzgyNqJc8vPHTE48=", - "dev": true, - "optional": true - }, - "uuid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", - "integrity": "sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w=", - "dev": true, - "optional": true - } - } - }, "@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -5592,7 +5516,7 @@ "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true, "optional": true }, @@ -9700,7 +9624,7 @@ "camel-case": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", "dev": true, "optional": true, "requires": { @@ -21436,12 +21360,6 @@ "is-wsl": "^2.1.1" } }, - "openzeppelin-solidity-2.3.0": { - "version": "npm:openzeppelin-solidity@2.3.0", - "resolved": "https://registry.npmjs.org/openzeppelin-solidity/-/openzeppelin-solidity-2.3.0.tgz", - "integrity": "sha512-QYeiPLvB1oSbDt6lDQvvpx7k8ODczvE474hb2kLXZBPKMsxKT1WxTCHBYrCU7kS7hfAku4DcJ0jqOyL+jvjwQw==", - "dev": true - }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -26093,14 +26011,14 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "optional": true }, "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", "dev": true, "optional": true }, @@ -27313,6 +27231,23 @@ "web3-utils": "1.2.2" }, "dependencies": { + "@chainlink/contracts-0.0.10": { + "version": "npm:@chainlink/contracts@0.0.10", + "resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.0.10.tgz", + "integrity": "sha512-ok+ucSQ+3mrR+zjbi6zIrdd5M9XymcqVcnXGVyqBVRYZp97jS2/rt/glP320JmHxmi4pacgDOg0Ux11xIr1S8Q==", + "dev": true, + "requires": { + "@truffle/contract": "^4.2.6", + "ethers": "^4.0.45" + } + }, + "aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "dev": true, + "optional": true + }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", @@ -27336,6 +27271,78 @@ "xhr-request-promise": "^0.1.2" } }, + "ethers": { + "version": "4.0.49", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.49.tgz", + "integrity": "sha512-kPltTvWiyu+OktYy1IStSO16i2e7cS9D9OxZ81q2UUaiNPVrm/RTcbxamCXF9VUSKzJIdJV68EAIhTEVBalRWg==", + "dev": true, + "optional": true, + "requires": { + "aes-js": "3.0.0", + "bn.js": "^4.11.9", + "elliptic": "6.5.4", + "hash.js": "1.1.3", + "js-sha3": "0.5.7", + "scrypt-js": "2.0.4", + "setimmediate": "1.0.4", + "uuid": "2.0.1", + "xmlhttprequest": "1.8.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true, + "optional": true + } + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "dev": true, + "optional": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" + } + }, + "js-sha3": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", + "integrity": "sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==", + "dev": true, + "optional": true + }, + "openzeppelin-solidity-2.3.0": { + "version": "npm:openzeppelin-solidity@2.3.0", + "resolved": "https://registry.npmjs.org/openzeppelin-solidity/-/openzeppelin-solidity-2.3.0.tgz", + "integrity": "sha512-QYeiPLvB1oSbDt6lDQvvpx7k8ODczvE474hb2kLXZBPKMsxKT1WxTCHBYrCU7kS7hfAku4DcJ0jqOyL+jvjwQw==", + "dev": true + }, + "scrypt-js": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.4.tgz", + "integrity": "sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw==", + "dev": true, + "optional": true + }, + "setimmediate": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.4.tgz", + "integrity": "sha512-/TjEmXQVEzdod/FFskf3o7oOAsGhHf2j1dZqRFbDzq4F3mvvxflIIi4Hd3bLQE9y/CpwqfSQam5JakI/mi3Pog==", + "dev": true, + "optional": true + }, + "uuid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", + "integrity": "sha512-nWg9+Oa3qD2CQzHIP4qKUqwNfzKn8P0LtFhotaCTFchsV7ZfDhAybeip/HZVeMIpZi9JgY1E3nUlwaCmZT1sEg==", + "dev": true, + "optional": true + }, "web3-utils": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.2.2.tgz", @@ -31468,9 +31475,9 @@ } }, "zokrates-js": { - "version": "1.0.43", - "resolved": "https://registry.npmjs.org/zokrates-js/-/zokrates-js-1.0.43.tgz", - "integrity": "sha512-34R5gZNEdpXevt3iV9h0nOr2Tf187BfLW54oNc0/xdDFBq6FxzqXsEw0YP5UJkm87zGGe9XNTyMv7mukb3BSGw==" + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/zokrates-js/-/zokrates-js-1.0.41.tgz", + "integrity": "sha512-lTBcmv3Tbk8MPphLP1t3ElRayXxKVMUQHOtCQdnTfVyeERHxcVfdaQ/ymTH3sqafSiiT5+F8zchfdZpC//LXuw==" } } } diff --git a/wallet/package.json b/wallet/package.json index cd9733f09..fcf54dca2 100644 --- a/wallet/package.json +++ b/wallet/package.json @@ -102,10 +102,10 @@ "webpack-dev-server": "3.11.1", "webpack-manifest-plugin": "2.2.0", "workbox-webpack-plugin": "5.1.4", - "zokrates-js": "1.0.43" + "zokrates-js": "1.0.41" }, "scripts": { - "start": "LOCAL_PROPOSER=true PUBLIC_URL='/nightfall/' USE_STUBS=true REACT_APP_MODE=local node scripts/start.js", + "start": "LOCAL_PROPOSER=true PUBLIC_URL='/nightfall/' USE_STUBS=false REACT_APP_MODE=local node scripts/start.js", "start:testnet": "LOCAL_PROPOSER=false PUBLIC_URL='/nightfall/' USE_STUBS=false REACT_APP_MODE=testnet env-cmd -f .testnet.env node scripts/start.js", "start:mainnet": "LOCAL_PROPOSER=false PUBLIC_URL='/nightfall/' USE_STUBS=false REACT_APP_MODE=mainnet env-cmd -f .mainnet.env node scripts/start.js", "build": "LOCAL_PROPOSER=false USE_STUBS=false REACT_APP_MODE=production PUBLIC_URL='https://wallet-beta.polygon.technology/nightfall/' node scripts/build.js", diff --git a/wallet/src/@types/general-number/index.d.ts b/wallet/src/@types/general-number/index.d.ts new file mode 100644 index 000000000..c24a81c4b --- /dev/null +++ b/wallet/src/@types/general-number/index.d.ts @@ -0,0 +1,42 @@ +declare module 'general-number' { + // export interface GeneralNumber { + // hex: string + // } + export function generalise(gn: Record[]) : Record[] + export function generalise(gn: T[]) : GeneralNumber[] + export function generalise(gn: Record) : Record + export function generalise(gn: T) : GeneralNumber + // export function generalise(gn: Array) : Array + class GeneralNumber { + get binary(): string + + get binaryArray(): string[] + + get bytes(): number[] + + // returns the decimal representation, as a String type. Synonymous with `integer()`. + get decimal(): string + + // returns the decimal representation, as a String type. Synonymous with `decimal()`. + get integer(): string + + // returns the decimal representation, as a Number type (if less than javascript's MAX_SAFE_INTEGER). (Otherwise it will throw). + get number(): number + + get bigInt(): bigint + + get boolean(): bigint + + get ascii(): string + + get utf8(): string + + // Safe fallback for accidentally calling '.all' on a GeneralNumber (rather than a GeneralObject, which actuallty supports this property) + get all(): GeneralNumber + + limbs(limbBitLength: number,numberOfLimbs: undefined | number, returnType?: string, throwErrors?: boolean): string[]; + hex(byteLength: number, butTruncateValueToByteLength?: number): string; + field(modulus: bigint, noOverflow?: boolean): string + } + export function stitchLimbs(_limbs: number[], _limbBits? : number) +} \ No newline at end of file diff --git a/wallet/src/@types/globals.d.ts b/wallet/src/@types/globals.d.ts new file mode 100644 index 000000000..c43602600 --- /dev/null +++ b/wallet/src/@types/globals.d.ts @@ -0,0 +1,23 @@ +// globals.d.ts +declare module globalThis { + var config = { + BN128_GROUP_ORDER: bigint, + BABYJUBJUB: { + JUBJUBA: bigint, + JUBJUBD: bigint, + INFINITY: [bigint,bigint], + GENERATOR: [bigint, bigint], + JUBJUBE: bigint, + JUBJUBC: bigint, + MONTA: bigint, + MONTB: bigint, + }, + COMMITMENTS_DB: string, + TIMBER_COLLECTION: string, + SUBMITTED_BLOCKS_COLLECTION: string, + TRANSACTIONS_COLLECTION: string, + COMMITMENTS_COLLECTION: string, + KEYS_COLLECTION: string, + CIRCUIT_COLLECTION: string, + } +} diff --git a/wallet/src/common-files/classes/transaction.js b/wallet/src/common-files/classes/transaction.js index 5b249350d..b1bbc4bdf 100644 --- a/wallet/src/common-files/classes/transaction.js +++ b/wallet/src/common-files/classes/transaction.js @@ -13,6 +13,13 @@ const { generalise } = gen; const TOKEN_TYPES = { ERC20: 0, ERC721: 1, ERC1155: 2 }; const { TRANSACTION_TYPES } = global.nightfallConstants; +const arrayEquality = (as, bs) => { + if (as.length === bs.length) { + return as.every(a => bs.includes(a)); + } + return false; +}; + // function to compute the keccak hash of a transaction function keccak(preimage) { const web3 = Web3.connection(); @@ -29,7 +36,7 @@ function keccak(preimage) { compressedSecrets, } = preimage; let { proof } = preimage; - proof = compressProof(proof); + proof = arrayEquality(proof, [0, 0, 0, 0, 0, 0, 0, 0]) ? [0, 0, 0, 0] : compressProof(proof); const transaction = [ value, historicRootBlockNumberL2, @@ -56,7 +63,7 @@ class Transaction { // them undefined work?) constructor({ fee, - historicRootBlockNumberL2, + historicRootBlockNumberL2: _historicRoot, transactionType, tokenType, tokenId, @@ -68,23 +75,27 @@ class Transaction { compressedSecrets: _compressedSecrets, // this must be array of objects that are compressed from Secrets class proof, // this must be a proof object, as computed by zokrates worker }) { - if (proof === undefined) throw new Error('Proof cannot be undefined'); - const flatProof = Object.values(proof).flat(Infinity); let commitments; let nullifiers; let compressedSecrets; + let flatProof; + let historicRootBlockNumberL2; + if (proof === undefined) flatProof = [0, 0, 0, 0, 0, 0, 0, 0]; + else flatProof = Object.values(proof).flat(Infinity); if (_commitments === undefined) commitments = [{ hash: 0 }, { hash: 0 }]; else if (_commitments.length === 1) commitments = [..._commitments, { hash: 0 }]; else commitments = _commitments; if (_nullifiers === undefined) nullifiers = [{ hash: 0 }, { hash: 0 }]; else if (_nullifiers.length === 1) nullifiers = [..._nullifiers, { hash: 0 }]; else nullifiers = _nullifiers; - if (_compressedSecrets === undefined) compressedSecrets = [0, 0, 0, 0, 0, 0, 0, 0]; + if (_compressedSecrets === undefined) compressedSecrets = [0, 0]; else compressedSecrets = _compressedSecrets; + if (_historicRoot === undefined) historicRootBlockNumberL2 = [0, 0]; + else if (_historicRoot.length === 1) historicRootBlockNumberL2 = [..._historicRoot, 0]; + else historicRootBlockNumberL2 = _historicRoot; - if ((transactionType === 0 || transactionType === 3) && TOKEN_TYPES[tokenType] === undefined) + if ((transactionType === 0 || transactionType === 2) && TOKEN_TYPES[tokenType] === undefined) throw new Error('Unrecognized token type'); - // convert everything to hex(32) for interfacing with web3 const preimage = generalise({ fee: fee || 0, @@ -143,7 +154,7 @@ class Transaction { commitments, nullifiers, compressedSecrets, - proof: compressProof(proof), + proof: arrayEquality(proof, [0, 0, 0, 0, 0, 0, 0, 0]) ? [0, 0, 0, 0] : compressProof(proof), }; } } diff --git a/wallet/src/common-files/utils/crypto/crypto-random.js b/wallet/src/common-files/utils/crypto/crypto-random.js index 5212f293a..640138c90 100644 --- a/wallet/src/common-files/utils/crypto/crypto-random.js +++ b/wallet/src/common-files/utils/crypto/crypto-random.js @@ -1,4 +1,4 @@ -// ignore unused exports default +// ignore unused exports /* eslint import/no-extraneous-dependencies: "off" */ /** @@ -8,11 +8,24 @@ Simple routine to create a cryptographically sound random. import crypto from 'crypto'; import gen from 'general-number'; -const { GN } = gen; +const { GN, generalise } = gen; -async function rand(bytes) { +export async function rand(bytes) { const buf = await crypto.randomBytes(bytes); return new GN(buf.toString('hex'), 'hex'); } -export default rand; +// Rejection sampling for a value < bigIntValue +export async function randValueLT(bigIntValue) { + let genVal = Infinity; + const MAX_ATTEMPTS = 1000; + const minimumBytes = Math.ceil(generalise(bigIntValue).binary.length / 8); + let counter = 0; + do { + // eslint-disable-next-line no-await-in-loop + genVal = await rand(minimumBytes); + counter++; + } while (genVal.bigInt >= bigIntValue || counter === MAX_ATTEMPTS); + if (counter === MAX_ATTEMPTS) throw new Error("Couldn't make a number below target value"); + return genVal; +} diff --git a/wallet/src/common-files/utils/crypto/merkle-tree/utils.js b/wallet/src/common-files/utils/crypto/merkle-tree/utils.js index d90e30ca9..2224af34e 100644 --- a/wallet/src/common-files/utils/crypto/merkle-tree/utils.js +++ b/wallet/src/common-files/utils/crypto/merkle-tree/utils.js @@ -8,12 +8,13 @@ import createKeccakHash from 'keccak'; import crypto from 'crypto'; import sb from 'safe-buffer'; -import { generalise } from 'general-number'; +import gen from 'general-number'; import mimcHashFunction from '../mimc/mimc.js'; import poseidonHashFunction from '../poseidon/poseidon.js'; const { Buffer } = sb; const { CURVE } = global.config; +const { generalise } = gen; function parseToDigitsArray(str, base) { const digits = str.split(''); diff --git a/wallet/src/common-files/utils/crypto/poseidon/poseidon.js b/wallet/src/common-files/utils/crypto/poseidon/poseidon.js index 2fa14ac71..d9abdf655 100644 --- a/wallet/src/common-files/utils/crypto/poseidon/poseidon.js +++ b/wallet/src/common-files/utils/crypto/poseidon/poseidon.js @@ -58,7 +58,6 @@ function poseidonHash(_inputs) { state = sbox(state, f, p, r); state = mix(state, m); } - // console.log('MATRIX', m); return generalise(state[0]); } diff --git a/wallet/src/common-files/utils/curve-maths/curves.js b/wallet/src/common-files/utils/curve-maths/curves.js index f4110b16d..cfa909a7e 100644 --- a/wallet/src/common-files/utils/curve-maths/curves.js +++ b/wallet/src/common-files/utils/curve-maths/curves.js @@ -5,11 +5,18 @@ module for manupulating elliptic curve points for an alt-bn128 curve. This is the curve that Ethereum currently has pairing precompiles for. All the return values are BigInts (or arrays of BigInts). */ +import utils from '../crypto/merkle-tree/utils'; import { mulMod, addMod, squareRootModPrime } from '../crypto/number-theory'; import Fq2 from '../../classes/fq2'; import Proof from '../../classes/proof'; +import { modDivide } from '../crypto/modular-division'; -const { BN128_PRIME_FIELD } = global.config; +const { BN128_PRIME_FIELD, BN128_GROUP_ORDER, BABYJUBJUB } = global.config; + +const one = BigInt(1); +const { JUBJUBE, JUBJUBC, JUBJUBD, JUBJUBA } = BABYJUBJUB; +const Fp = BN128_GROUP_ORDER; // the prime field used with the curve E(Fp) +const Fq = JUBJUBE / JUBJUBC; /** function to compress a G1 point. If we throw away the y coodinate, we can @@ -103,3 +110,86 @@ export function decompressProof(compressedProof) { decompressG1(cCompressed), ].flat(2); } + +function isOnCurve(p) { + const { JUBJUBA: a, JUBJUBD: d } = BABYJUBJUB; + const uu = (p[0] * p[0]) % Fp; + const vv = (p[1] * p[1]) % Fp; + const uuvv = (uu * vv) % Fp; + return (a * uu + vv) % Fp === (one + d * uuvv) % Fp; +} + +/** +Point addition on the babyjubjub curve TODO - MOD P THIS +*/ +export function add(p, q) { + const { JUBJUBA: a, JUBJUBD: d } = BABYJUBJUB; + const u1 = p[0]; + const v1 = p[1]; + const u2 = q[0]; + const v2 = q[1]; + const uOut = modDivide(u1 * v2 + v1 * u2, one + d * u1 * u2 * v1 * v2, Fp); + const vOut = modDivide(v1 * v2 - a * u1 * u2, one - d * u1 * u2 * v1 * v2, Fp); + if (!isOnCurve([uOut, vOut])) throw new Error('Addition point is not on the babyjubjub curve'); + return [uOut, vOut]; +} + +/** +Scalar multiplication on a babyjubjub curve +@param {String} scalar - scalar mod q (will wrap if greater than mod q, which is probably ok) +@param {Object} h - curve point in u,v coordinates +*/ +export function scalarMult(scalar, h, form = 'Edwards') { + const { INFINITY } = BABYJUBJUB; + const a = ((BigInt(scalar) % Fq) + Fq) % Fq; // just in case we get a value that's too big or negative + const exponent = a.toString(2).split(''); // extract individual binary elements + let doubledP = [...h]; // shallow copy h to prevent h being mutated by the algorithm + let accumulatedP = INFINITY; + for (let i = exponent.length - 1; i >= 0; i--) { + const candidateP = add(accumulatedP, doubledP, form); + accumulatedP = exponent[i] === '1' ? candidateP : accumulatedP; + doubledP = add(doubledP, doubledP, form); + } + if (!isOnCurve(accumulatedP)) + throw new Error('Scalar multiplication point is not on the babyjubjub curve'); + return accumulatedP; +} + +/** A useful function that takes a curve point and throws away the x coordinate +retaining only the y coordinate and the odd/eveness of the x coordinate (plays the +part of a sign in mod arithmetic with a prime field). This loses no information +because we know the curve that relates x to y and the odd/eveness disabiguates the two +possible solutions. So it's a useful data compression. +TODO - probably simpler to use integer arithmetic rather than binary manipulations +*/ +export function edwardsCompress(p) { + const px = p[0]; + const py = p[1]; + const xBits = px.toString(2).padStart(256, '0'); + const yBits = py.toString(2).padStart(256, '0'); + const sign = xBits[255] === '1' ? '1' : '0'; + const yBitsC = sign.concat(yBits.slice(1)); // add in the sign bit + const y = utils.ensure0x(BigInt('0b'.concat(yBitsC)).toString(16).padStart(64, '0')); // put yBits into hex + return y; +} + +export function edwardsDecompress(y) { + const py = BigInt(y).toString(2).padStart(256, '0'); + const sign = py[0]; + const yfield = BigInt(`0b${py.slice(1)}`); // remove the sign encoding + if (yfield > Fp || yfield < 0) throw new Error(`y cordinate ${yfield} is not a field element`); + // 168700.x^2 + y^2 = 1 + 168696.x^2.y^2 + const y2 = mulMod([yfield, yfield], Fp); + const x2 = modDivide( + addMod([y2, BigInt(-1)], Fp), + addMod([mulMod([JUBJUBD, y2], Fp), -JUBJUBA], Fp), + Fp, + ); + if (x2 === 0n && sign === '0') return BABYJUBJUB.INFINITY; + let xfield = squareRootModPrime(x2, Fp); + const px = BigInt(xfield).toString(2).padStart(256, '0'); + if (px[255] !== sign) xfield = Fp - xfield; + const p = [xfield, yfield]; + if (!isOnCurve(p)) throw new Error('The computed point was not on the Babyjubjub curve'); + return p; +} diff --git a/wallet/src/components/Assets/index.jsx b/wallet/src/components/Assets/index.jsx index 56e6e5bad..d3f5927e7 100644 --- a/wallet/src/components/Assets/index.jsx +++ b/wallet/src/components/Assets/index.jsx @@ -130,10 +130,10 @@ function ReceiveModal(props) {
- +

Wallet Address

- {state.compressedPkd} + {state.compressedZkpPublicKey} {copied ? ( ) : ( - setCopied(true)}> + setCopied(true)}> Copy Address diff --git a/wallet/src/components/BridgeComponent/index.jsx b/wallet/src/components/BridgeComponent/index.jsx index 0c98f87ac..c87caffef 100644 --- a/wallet/src/components/BridgeComponent/index.jsx +++ b/wallet/src/components/BridgeComponent/index.jsx @@ -8,9 +8,9 @@ import importTokens from '@TokenList/index'; import deposit from '@Nightfall/services/deposit'; import withdraw from '@Nightfall/services/withdraw'; import { getWalletBalance } from '@Nightfall/services/commitment-storage'; -import { decompressKey } from '@Nightfall/services/keys'; import { saveTransaction } from '@Nightfall/services/database'; import Lottie from 'lottie-react'; +import { generalise } from 'general-number'; import ethChainImage from '../../assets/img/ethereum-chain.svg'; import polygonNightfall from '../../assets/svg/polygon-nightfall.svg'; import discloserBottomImage from '../../assets/img/discloser-bottom.svg'; @@ -122,11 +122,6 @@ const supportedTokens = importTokens(); const { proposerUrl } = global.config; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const gen = require('general-number'); - -const { generalise } = gen; - const BridgeComponent = () => { const [state] = useContext(UserContext); const { accountInstance } = useAccount(); @@ -229,10 +224,9 @@ const BridgeComponent = () => { async function triggerTx() { if (shieldContractAddress === '') setShieldAddress(shieldAddressGet()); const ercAddress = token.address; - const zkpKeys = await retrieveAndDecrypt(state.compressedPkd); + const zkpKeys = await retrieveAndDecrypt(state.compressedZkpPublicKey); switch (txType) { case 'deposit': { - const pkd = decompressKey(generalise(state.compressedPkd)); await approve( ercAddress, shieldContractAddress, @@ -249,8 +243,8 @@ const BridgeComponent = () => { ercAddress, tokenId: 0, value: new BigFloat(transferValue, token.decimals).toBigInt().toString(), - pkd, - nsk: zkpKeys.nsk, + compressedZkpPublicKey: state.compressedZkpPublicKey, + nullifierKey: zkpKeys.nullifierKey, fee: 1, tokenType: 'ERC20', }, @@ -284,8 +278,8 @@ const BridgeComponent = () => { tokenId: 0, value: new BigFloat(transferValue, token.decimals).toBigInt().toString(), recipientAddress: await Web3.getAccount(), - nsk: zkpKeys.nsk, - ask: zkpKeys.ask, + nullifierKey: generalise(zkpKeys.nullifierKey), + rootKey: zkpKeys.rootKey, tokenType: 'ERC20', fees: 1, }, @@ -359,9 +353,9 @@ const BridgeComponent = () => { async function updateL2Balance() { if (token && token.address) { - const l2bal = await getWalletBalance(state.compressedPkd); - if (Object.hasOwnProperty.call(l2bal, state.compressedPkd)) - setL2Balance(l2bal[state.compressedPkd][token.address.toLowerCase()] ?? 0n); + const l2bal = await getWalletBalance(state.compressedZkpPublicKey); + if (Object.hasOwnProperty.call(l2bal, state.compressedZkpPublicKey)) + setL2Balance(l2bal[state.compressedZkpPublicKey][token.address.toLowerCase()] ?? 0n); else setL2Balance(0n); } } diff --git a/wallet/src/components/Header/navItems.jsx b/wallet/src/components/Header/navItems.jsx index db1013923..9e50a529c 100644 --- a/wallet/src/components/Header/navItems.jsx +++ b/wallet/src/components/Header/navItems.jsx @@ -17,9 +17,11 @@ export default function NavItems() { {!isSmallScreen && (
- {state.compressedPkd && ( + {state.compressedZkpPublicKey && (
- {`${state.compressedPkd.slice(0, 6)}...${state.compressedPkd.slice(-6)}`} + {`${state.compressedZkpPublicKey.slice(0, 6)}...${state.compressedZkpPublicKey.slice( + -6, + )}`}
)} diff --git a/wallet/src/components/Modals/sendModal.tsx b/wallet/src/components/Modals/sendModal.tsx index 5993815de..e6ba74a67 100644 --- a/wallet/src/components/Modals/sendModal.tsx +++ b/wallet/src/components/Modals/sendModal.tsx @@ -14,7 +14,7 @@ import Lottie from 'lottie-react'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import { generalise } from 'general-number'; -import { decompressKey } from '@Nightfall/services/keys'; +import { ZkpKeys } from '@Nightfall/services/keys'; import { UserContext } from '../../hooks/User'; import maticImg from '../../assets/img/polygon-chain.svg'; import { retrieveAndDecrypt } from '../../utils/lib/key-storage'; @@ -401,10 +401,10 @@ const SendModal = (props: SendModalProps): JSX.Element => { useEffect(() => { const getBalance = async () => { const l2bal: Record> = await getWalletBalance( - state?.compressedPkd, + state?.compressedZkpPublicKey, ); - if (Object.hasOwnProperty.call(l2bal, state?.compressedPkd)) - setL2Balance(l2bal[state.compressedPkd][sendToken.address.toLowerCase()] ?? 0n); + if (Object.hasOwnProperty.call(l2bal, state?.compressedZkpPublicKey)) + setL2Balance(l2bal[state.compressedZkpPublicKey][sendToken.address.toLowerCase()] ?? 0n); else setL2Balance(0n); }; getBalance(); @@ -475,7 +475,7 @@ const SendModal = (props: SendModalProps): JSX.Element => { useEffect(() => { try { - decompressKey(generalise(recipient)); + ZkpKeys.decompressZkpPublicKey(generalise(recipient)); setIsValidAddress(true); } catch { setIsValidAddress(false); @@ -505,7 +505,7 @@ const SendModal = (props: SendModalProps): JSX.Element => { if (shieldContractAddress === '') setShieldAddress(shieldAddressGet()); setShowModalConfirm(true); setShowModalTransferInProgress(true); - const { nsk, ask } = await retrieveAndDecrypt(state.compressedPkd); + const { nullifierKey, rootKey } = await retrieveAndDecrypt(state.compressedZkpPublicKey); await timeout(2000); setShowModalTransferInProgress(false); setShowModalTransferEnRoute(true); @@ -515,11 +515,12 @@ const SendModal = (props: SendModalProps): JSX.Element => { ercAddress: sendToken.address, tokenId: 0, recipientData: { - recipientCompressedPkds: [recipient], + recipientCompressedZkpPublicKeys: [recipient], values: [new BigFloat(valueToSend, sendToken.decimals).toBigInt().toString()], }, - nsk, - ask, + nullifierKey, + rootKey, + compressedZkpPublicKey: state.compressedZkpPublicKey, fee: 0, }, shieldContractAddress, @@ -599,7 +600,7 @@ const SendModal = (props: SendModalProps): JSX.Element => { type="text" placeholder="Enter a Nightfall Address" onChange={e => setRecipient(e.target.value)} - id="TokenItem_modalSend_compressedPkd" + id="TokenItem_modalSend_compressedZkpPublicKey" /> {!isValidAddress && (

diff --git a/wallet/src/components/TokenItem/index.jsx b/wallet/src/components/TokenItem/index.jsx index 29cdddff3..77f41d077 100644 --- a/wallet/src/components/TokenItem/index.jsx +++ b/wallet/src/components/TokenItem/index.jsx @@ -18,13 +18,14 @@ export default function TokenItem(props) { const [filteredTokens, setFilteredTokens] = useState(supportedTokens); useEffect(async () => { - const l2bal = await getWalletBalance(state.compressedPkd); - if (Object.hasOwnProperty.call(l2bal, state.compressedPkd)) + const l2bal = await getWalletBalance(state.compressedZkpPublicKey); + console.log('Wallet Balance', l2bal); + if (Object.hasOwnProperty.call(l2bal, state.compressedZkpPublicKey)) setFilteredTokens( filteredTokens.map(t => { return { ...t, - l2Balance: l2bal[state.compressedPkd][t.address.toLowerCase()] ?? 0, + l2Balance: l2bal[state.compressedZkpPublicKey][t.address.toLowerCase()] ?? 0, }; }), ); diff --git a/wallet/src/components/Tokens/index.jsx b/wallet/src/components/Tokens/index.jsx index c76ff5d58..6234421b9 100644 --- a/wallet/src/components/Tokens/index.jsx +++ b/wallet/src/components/Tokens/index.jsx @@ -16,16 +16,19 @@ export default function Tokens(token) {

Balances on Polygon Nightfall
- {state.compressedPkd && ( + {state.compressedZkpPublicKey && (
- {`${state.compressedPkd.slice(0, 6)}...${state.compressedPkd.slice(-6)}`} + {`${state.compressedZkpPublicKey.slice( + 0, + 6, + )}...${state.compressedZkpPublicKey.slice(-6)}`}
)}
- {/*
{`Nightfall address: ${state.compressedPkd.slice(0, 6)}...${state.compressedPkd.slice(-6)}`}
*/} + {/*
{`Nightfall address: ${state.compressedZkpPublicKey.slice(0, 6)}...${state.compressedZkpPublicKey.slice(-6)}`}
*/}
diff --git a/wallet/src/components/Transactions/index.jsx b/wallet/src/components/Transactions/index.jsx index 9b6aa802d..c6e418270 100644 --- a/wallet/src/components/Transactions/index.jsx +++ b/wallet/src/components/Transactions/index.jsx @@ -22,13 +22,8 @@ const supportedTokens = importTokens(); const { SHIELD_CONTRACT_NAME, ZERO } = global.nightfallConstants; -const txTypeOptions = ['Deposit', 'Transfer', 'Transfer', 'Withdraw']; -const txTypeDest = [ - 'From Ethereum to L2', - 'Private Transfer', - 'Private Transfer', - 'From L2 to Ethereum', -]; +const txTypeOptions = ['Deposit', 'Transfer', 'Withdraw']; +const txTypeDest = ['From Ethereum to L2', 'Private Transfer', 'From L2 to Ethereum']; const displayTime = (start, end) => { const diff = Number(end) - Number(start); @@ -87,7 +82,7 @@ const Transactions = () => { // The value of transfers need to be derived from the components making up the transfer // Add sum nullifiers in transactions // Subtract sum of commitments we have. - if (safeTransactionType === '1' || safeTransactionType === '2') + if (safeTransactionType === '1') commitmentsDB.forEach(c => { if (tx.nullifiers.includes(c.nullifier)) value -= BigInt(c.preimage.value); else if (tx.commitments.includes(c._id)) value += BigInt(c.preimage.value); @@ -114,7 +109,7 @@ const Transactions = () => { let withdrawReady = false; if ( - safeTransactionType === '3' && + safeTransactionType === '2' && tx.isOnChain > 0 && tx.withdrawState !== 'finalised' && Math.floor(Date.now() / 1000) - tx.createdTime > 3600 * 24 * 7 @@ -224,9 +219,9 @@ const Transactions = () => { case 'deposit': return f.txType === '0'; case 'transfer': - return f.txType === '1' || f.txType === '2'; + return f.txType === '1'; case 'withdraw': - return f.txType === '3'; + return f.txType === '2'; case 'pending': return f.isOnChain === -1; default: diff --git a/wallet/src/contract-abis/Shield.json b/wallet/src/contract-abis/Shield.json index 39c466b37..0fc7646db 100644 --- a/wallet/src/contract-abis/Shield.json +++ b/wallet/src/contract-abis/Shield.json @@ -24,6 +24,19 @@ "name": "CommittedToChallenge", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -49,6 +62,32 @@ "name": "InstantWithdrawalRequested", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "NewBootChallengerSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "NewBootProposerSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -135,25 +174,45 @@ "anonymous": false, "inputs": [ { - "indexed": true, - "internalType": "bytes32", - "name": "blockHash", - "type": "bytes32" - }, + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { "indexed": false, "internalType": "uint256", "name": "blockNumberL2", "type": "uint256" + } + ], + "name": "Rollback", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "ercAddress", + "type": "address" }, { "indexed": false, "internalType": "uint256", - "name": "leafCount", + "name": "amount", "type": "uint256" } ], - "name": "Rollback", + "name": "ShieldBalanceTransferred", "type": "event" }, { @@ -162,6 +221,19 @@ "name": "TransactionSubmitted", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -336,6 +408,27 @@ "type": "function", "constant": true }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function", + "constant": true + }, { "inputs": [ { @@ -360,11 +453,6 @@ "internalType": "address", "name": "tokenAddr", "type": "address" - }, - { - "internalType": "uint256", - "name": "transactionType", - "type": "uint256" } ], "name": "removeRestriction", @@ -461,6 +549,13 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -563,9 +658,9 @@ "type": "bytes32[2]" }, { - "internalType": "bytes32[8]", + "internalType": "bytes32[2]", "name": "compressedSecrets", - "type": "bytes32[8]" + "type": "bytes32[2]" }, { "internalType": "uint256[4]", @@ -671,9 +766,9 @@ "type": "bytes32[2]" }, { - "internalType": "bytes32[8]", + "internalType": "bytes32[2]", "name": "compressedSecrets", - "type": "bytes32[8]" + "type": "bytes32[2]" }, { "internalType": "uint256[4]", @@ -851,9 +946,9 @@ "type": "bytes32[2]" }, { - "internalType": "bytes32[8]", + "internalType": "bytes32[2]", "name": "compressedSecrets", - "type": "bytes32[8]" + "type": "bytes32[2]" }, { "internalType": "uint256[4]", @@ -975,9 +1070,9 @@ "type": "bytes32[2]" }, { - "internalType": "bytes32[8]", + "internalType": "bytes32[2]", "name": "compressedSecrets", - "type": "bytes32[8]" + "type": "bytes32[2]" }, { "internalType": "uint256[4]", @@ -1055,9 +1150,9 @@ "type": "bytes32[2]" }, { - "internalType": "bytes32[8]", + "internalType": "bytes32[2]", "name": "compressedSecrets", - "type": "bytes32[8]" + "type": "bytes32[2]" }, { "internalType": "uint256[4]", @@ -1162,9 +1257,9 @@ "type": "bytes32[2]" }, { - "internalType": "bytes32[8]", + "internalType": "bytes32[2]", "name": "compressedSecrets", - "type": "bytes32[8]" + "type": "bytes32[2]" }, { "internalType": "uint256[4]", diff --git a/wallet/src/hooks/User/index.jsx b/wallet/src/hooks/User/index.jsx index 4f18b6671..bcfabf552 100644 --- a/wallet/src/hooks/User/index.jsx +++ b/wallet/src/hooks/User/index.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { useHistory } from 'react-router-dom'; -import { generateKeys } from '@Nightfall/services/keys'; +import { ZkpKeys } from '@Nightfall/services/keys'; import blockProposedEventHandler from '@Nightfall/event-handlers/block-proposed'; import { checkIndexDBForCircuit, getMaxBlock } from '@Nightfall/services/database'; import * as Storage from '../../utils/lib/local-storage'; @@ -11,7 +11,7 @@ import useInterval from '../useInterval'; const { eventWsUrl, USE_STUBS } = global.config; export const initialState = { - compressedPkd: '', + compressedZkpPublicKey: '', chainSync: false, circuitSync: false, }; @@ -30,31 +30,31 @@ export const UserProvider = ({ children }) => { const deriveAccounts = async (mnemonic, numAccts) => { const accountRange = Array.from({ length: numAccts }, (v, i) => i); const zkpKeys = await Promise.all( - accountRange.map(i => generateKeys(mnemonic, `m/44'/60'/0'/${i.toString()}`)), + accountRange.map(i => ZkpKeys.generateZkpKeysFromMnemonic(mnemonic, i)), ); const aesGenParams = { name: 'AES-GCM', length: 128 }; const key = await crypto.subtle.generateKey(aesGenParams, false, ['encrypt', 'decrypt']); await storeBrowserKey(key); await Promise.all(zkpKeys.map(zkpKey => encryptAndStore(zkpKey))); - Storage.pkdArraySet( + Storage.ZkpPubKeyArraySet( '', - zkpKeys.map(z => z.compressedPkd), + zkpKeys.map(z => z.compressedZkpPublicKey), ); setState(previousState => { return { ...previousState, - compressedPkd: zkpKeys[0].compressedPkd, + compressedZkpPublicKey: zkpKeys[0].compressedZkpPublicKey, }; }); }; const syncState = async () => { - const pkds = Storage.pkdArrayGet(''); - if (pkds) { + const compressedZkpPublicKeys = Storage.ZkpPubKeyArrayGet(''); + if (compressedZkpPublicKeys) { setState(previousState => { return { ...previousState, - compressedPkd: pkds[0], + compressedZkpPublicKey: compressedZkpPublicKeys[0], }; }); } @@ -81,8 +81,8 @@ export const UserProvider = ({ children }) => { let messageEventHandler; const configureMessageListener = () => { - const { compressedPkd, socket } = state; - if (compressedPkd === '') return; + const { compressedZkpPublicKey, socket } = state; + if (compressedZkpPublicKey === '') return; if (messageEventHandler) { socket.removeEventListener('message', messageEventHandler); @@ -92,13 +92,13 @@ export const UserProvider = ({ children }) => { messageEventHandler = async function (event) { console.log('Message from server ', JSON.parse(event.data)); const parsed = JSON.parse(event.data); - const { ivk, nsk } = await retrieveAndDecrypt(compressedPkd); + const { nullifierKey, zkpPrivateKey } = await retrieveAndDecrypt(compressedZkpPublicKey); if (parsed.type === 'sync') { await parsed.historicalData .sort((a, b) => a.block.blockNumberL2 - b.block.blockNumberL2) .reduce(async (acc, curr) => { await acc; // Acc is a promise so we await it before processing the next one; - return blockProposedEventHandler(curr, [ivk], [nsk]); // TODO Should be array + return blockProposedEventHandler(curr, [zkpPrivateKey], [nullifierKey]); // TODO Should be array }, Promise.resolve()); if (Number(parsed.maxBlock) !== 1) { socket.send( @@ -117,7 +117,7 @@ export const UserProvider = ({ children }) => { }; }); } else if (parsed.type === 'blockProposed') - await blockProposedEventHandler(parsed.data, [ivk], [nsk]); + await blockProposedEventHandler(parsed.data, [zkpPrivateKey], [nullifierKey]); // TODO Rollback Handler }; @@ -129,7 +129,7 @@ export const UserProvider = ({ children }) => { }, []); React.useEffect(async () => { - if (state.compressedPkd === '') { + if (state.compressedZkpPublicKey === '') { console.log('Sync State'); await syncState(); } @@ -138,7 +138,7 @@ export const UserProvider = ({ children }) => { React.useEffect(() => { configureMessageListener(); - }, [state.compressedPkd]); + }, [state.compressedZkpPublicKey]); useInterval( async () => { diff --git a/wallet/src/nightfall-browser/classes/commitment.js b/wallet/src/nightfall-browser/classes/commitment.js index 068d89779..26b78b4d4 100644 --- a/wallet/src/nightfall-browser/classes/commitment.js +++ b/wallet/src/nightfall-browser/classes/commitment.js @@ -4,9 +4,11 @@ A commitment class */ import gen from 'general-number'; -import sha256 from '../../common-files/utils/crypto/sha256'; +import poseidon from '../../common-files/utils/crypto/poseidon/poseidon'; +import { ZkpKeys } from '../services/keys'; const { generalise } = gen; +const { BN128_GROUP_ORDER } = global.config; class Commitment { preimage; @@ -17,30 +19,34 @@ class Commitment { isNullifiedOnChain = -1; - constructor({ ercAddress, tokenId, value, pkd = [], compressedPkd, salt }) { - const items = { ercAddress, tokenId, value, pkd, compressedPkd, salt }; + constructor({ ercAddress, tokenId, value, zkpPublicKey, salt }) { + const items = { ercAddress, tokenId, value, zkpPublicKey, salt }; const keys = Object.keys(items); for (const key of keys) if (items[key] === undefined) throw new Error( `Property ${key} was undefined. Did you pass the wrong object to the constructor?`, ); - this.preimage = generalise({ - ercAddress, - tokenId, - value, - pkd, - compressedPkd, - salt, - }); - this.hash = generalise( - sha256([ - this.preimage.ercAddress, - this.preimage.tokenId, - this.preimage.value, - this.compressedPkd, - this.preimage.salt, - ]).hex(32, 31), + + // the compressedZkpPublicKey is not part of the pre-image but it's used widely in the rest of + // the code, so we hold it in the commitment object (but not as part of the preimage) + this.preimage = generalise(items); + this.compressedZkpPublicKey = + this.preimage.zkpPublicKey[0] === 0 + ? [0, 0] + : ZkpKeys.compressZkpPublicKey(this.preimage.zkpPublicKey); + // we encode the top four bytes of the tokenId into the empty bytes at the top of the erc address. + // this is consistent to what we do in the ZKP circuits + const [top4Bytes, remainder] = this.preimage.tokenId.limbs(224, 2).map(l => BigInt(l)); + const SHIFT = 1461501637330902918203684832716283019655932542976n; + this.hash = poseidon( + generalise([ + this.preimage.ercAddress.bigInt + top4Bytes * SHIFT, + remainder, + this.preimage.value.field(BN128_GROUP_ORDER), + ...this.preimage.zkpPublicKey.all.field(BN128_GROUP_ORDER), + this.preimage.salt.field(BN128_GROUP_ORDER), + ]), ); } diff --git a/wallet/src/nightfall-browser/classes/index.js b/wallet/src/nightfall-browser/classes/index.js index dfa722f9b..5a0a4faea 100644 --- a/wallet/src/nightfall-browser/classes/index.js +++ b/wallet/src/nightfall-browser/classes/index.js @@ -3,6 +3,5 @@ import Transaction from '../../common-files/classes/transaction'; import Commitment from './commitment'; import Nullifier from './nullifier'; -import Secrets from './secrets'; -export { Transaction, Commitment, Nullifier, Secrets }; +export { Transaction, Commitment, Nullifier }; diff --git a/wallet/src/nightfall-browser/classes/nullifier.js b/wallet/src/nightfall-browser/classes/nullifier.js index b6f4c1131..a8da21069 100644 --- a/wallet/src/nightfall-browser/classes/nullifier.js +++ b/wallet/src/nightfall-browser/classes/nullifier.js @@ -1,10 +1,9 @@ // ignore unused exports default - /** A nullifier class */ import gen from 'general-number'; -import sha256 from '../../common-files/utils/crypto/sha256'; +import poseidon from '../../common-files/utils/crypto/poseidon/poseidon'; const { generalise } = gen; @@ -13,12 +12,12 @@ class Nullifier { hash; - constructor(commitment, nsk) { + constructor(commitment, nullifierKey) { this.preimage = generalise({ - nsk, + nullifierKey, commitment: commitment.hash, }); - this.hash = generalise(sha256([this.preimage.nsk, this.preimage.commitment]).hex(32, 31)); + this.hash = poseidon([this.preimage.nullifierKey, this.preimage.commitment]); } } diff --git a/wallet/src/nightfall-browser/classes/secrets.js b/wallet/src/nightfall-browser/classes/secrets.js deleted file mode 100644 index 69944b829..000000000 --- a/wallet/src/nightfall-browser/classes/secrets.js +++ /dev/null @@ -1,110 +0,0 @@ -// ignore unused exports default - -/** - A secrets class - */ -import { GN, generalise } from 'general-number'; -import rand from '../../common-files/utils/crypto/crypto-random'; -import Commitment from './commitment'; -import { enc, dec, edwardsCompress, edwardsDecompress } from '../utils/crypto/encryption/elgamal'; -import { calculatePkd } from '../services/keys'; - -const { BN128_GROUP_ORDER } = global.config; -const { ZKP_KEY_LENGTH } = global.nightfallConstants; - -class Secrets { - ephemeralKeys; // random secret used in shared secret creation - - cipherText; - - squareRootsElligator2; // instead of calculating a square root using Tonelli Shanks algorithm which is required in Elligator 2, - // we pass the square root and prove that it is indeed the square root of a square number. We do this because Tonelli Shanks require - // modular exponentiation and we can't do dynamic exponent modular exponentiation in a circuit - - compressedSecrets; - - constructor(ephemeralKeys, cipherText, squareRootsElligator2) { - this.ephemeralKeys = generalise(ephemeralKeys); - this.cipherText = generalise(cipherText); - this.squareRootsElligator2 = generalise(squareRootsElligator2); - this.compressedSecrets = generalise( - cipherText.map(text => { - return edwardsCompress([text[0].bigInt, text[1].bigInt]); - }), - ); - } - - // function used to compress secrets to save gas on chain - static compressSecrets(secrets) { - return generalise( - secrets.cipherText.map(text => { - return edwardsCompress([text[0].bigInt, text[1].bigInt]); - }), - ); - } - - // function used to decompress compressed secrets - static decompressSecrets(secrets) { - return generalise(secrets).map(secret => { - return edwardsDecompress(secret.bigInt); - }); - } - - // function to encrypt secrets - static async encryptSecrets(messages, publicKey) { - let ephemeralKeys = []; - while (ephemeralKeys.length < 4) ephemeralKeys.push(rand(ZKP_KEY_LENGTH)); - ephemeralKeys = (await Promise.all(ephemeralKeys)).map(key => key.bigInt); - - const { cipherText, squareRootsElligator2 } = enc(ephemeralKeys, messages, publicKey); - const compressedSecrets = cipherText.map(text => { - return edwardsCompress([text[0], text[1]]); - }); - - return { - ephemeralKeys: generalise(ephemeralKeys), - cipherText: generalise(cipherText), - squareRootsElligator2: generalise(squareRootsElligator2), - compressedSecrets: generalise(compressedSecrets), - }; - } - - // function to decrypt secrets - static decryptSecrets(cipherText, privateKey, newCommitment) { - const tokenIdProbable = []; - const saltProbable = []; - try { - const decryptedMessages = dec(cipherText, privateKey); - const ercAddress = generalise(decryptedMessages[0]).hex(32); - // Since the encrypted message could be an encryption of the positive or the negative congruent form of the same number, we check which of the two - // satisfy commitment calculation. We do this for both token ID and salt - tokenIdProbable.push(decryptedMessages[1]); - tokenIdProbable.push((BN128_GROUP_ORDER - BigInt(decryptedMessages[1])) % BN128_GROUP_ORDER); - const value = decryptedMessages[2]; - saltProbable.push(decryptedMessages[3]); - saltProbable.push((BN128_GROUP_ORDER - BigInt(decryptedMessages[3])) % BN128_GROUP_ORDER); - const { pkd, compressedPkd } = calculatePkd(new GN(privateKey)); - let commitment = {}; - for (let i = 0; i < tokenIdProbable.length; i++) { - for (let j = 0; j < saltProbable.length; j++) { - const commitmentProbable = new Commitment({ - compressedPkd, - pkd, - ercAddress, - tokenId: tokenIdProbable[i], - value, - salt: saltProbable[j], - }); - if (commitmentProbable.hash.hex(32) === newCommitment) { - commitment = commitmentProbable; - } - } - } - return commitment; - } catch (err) { - throw new Error('Decryption error', err); - } - } -} - -export default Secrets; diff --git a/wallet/src/nightfall-browser/event-handlers/block-proposed.js b/wallet/src/nightfall-browser/event-handlers/block-proposed.js index 6ac2cd600..bc14bf0db 100644 --- a/wallet/src/nightfall-browser/event-handlers/block-proposed.js +++ b/wallet/src/nightfall-browser/event-handlers/block-proposed.js @@ -1,5 +1,9 @@ // ignore unused exports default +import { ZkpKeys } from '@Nightfall/services/keys'; +import { Commitment } from '@Nightfall/classes'; +import { decrypt, packSecrets } from '@Nightfall/services/kem-dem'; +import { generalise } from 'general-number'; import logger from '../../common-files/utils/logger'; import Timber from '../../common-files/classes/timber'; import { @@ -12,9 +16,6 @@ import { countWithdrawTransactionHashes, isTransactionHashWithdraw, } from '../services/commitment-storage'; -// import getProposeBlockCalldata from '../services/process-calldata'; -import Secrets from '../classes/secrets'; -// import { ivks, nsks } from '../services/keys'; import { getTreeByBlockNumberL2, saveTree, @@ -23,6 +24,7 @@ import { setTransactionHashSiblingInfo, updateTransactionTime, } from '../services/database'; +import { edwardsDecompress } from '../../common-files/utils/curve-maths/curves'; const { TIMBER_HEIGHT, TXHASH_TREE_HEIGHT, HASH_TYPE, TXHASH_TREE_HASH_TYPE } = global.config; const { ZERO } = global.nightfallConstants; @@ -30,9 +32,8 @@ const { ZERO } = global.nightfallConstants; /** This handler runs whenever a BlockProposed event is emitted by the blockchain */ -async function blockProposedEventHandler(data, ivks, nsks) { +async function blockProposedEventHandler(data, zkpPrivateKeys, nullifierKeys) { console.log(`Received Block Proposed event: ${JSON.stringify(data)}`); - // ivk will be used to decrypt secrets whilst nsk will be used to calculate nullifiers for commitments and store them const { blockNumber: currentBlockCount, transactionHash: transactionHashL1 } = data; const { transactions, block, blockTimestamp } = data; const latestTree = await getTreeByBlockNumberL2(block.blockNumberL2 - 1); @@ -49,20 +50,38 @@ async function blockProposedEventHandler(data, ivks, nsks) { (Number(transaction.transactionType) === 1 || Number(transaction.transactionType) === 2) && (await countCommitments(nonZeroCommitments)) === 0 ) { - ivks.forEach((key, i) => { + zkpPrivateKeys.forEach((key, i) => { // decompress the secrets first and then we will decryp t the secrets from this - const decompressedSecrets = Secrets.decompressSecrets(transaction.compressedSecrets); + const { zkpPublicKey } = ZkpKeys.calculateZkpPublicKey(generalise(key)); try { - const commitment = Secrets.decryptSecrets( - decompressedSecrets, - key, - nonZeroCommitments[0], + const cipherTexts = [ + transaction.ercAddress, + transaction.tokenId, + ...transaction.compressedSecrets, + ]; + const [packedErc, unpackedTokenID, ...rest] = decrypt( + generalise(key), + generalise(edwardsDecompress(transaction.recipientAddress)), + generalise(cipherTexts), ); - if (Object.keys(commitment).length === 0) - logger.info("This encrypted message isn't for this recipient"); - else { + const [erc, tokenId] = packSecrets( + generalise(packedErc), + generalise(unpackedTokenID), + 2, + 0, + ); + const plainTexts = generalise([erc, tokenId, ...rest]); + const commitment = new Commitment({ + zkpPublicKey, + ercAddress: plainTexts[0].bigInt, + tokenId: plainTexts[1].bigInt, + value: plainTexts[2].bigInt, + salt: plainTexts[3].bigInt, + }); + if (commitment.hash.hex(32) === nonZeroCommitments[0]) { isTxDecrypt = true; - storeCommitments.push(storeCommitment(commitment, nsks[i])); + logger.info('Successfully decrypted commitment for this recipient'); + storeCommitments.push(storeCommitment(commitment, nullifierKeys[i])); tempTransactionStore.push( saveTransaction({ transactionHashL1, @@ -71,8 +90,8 @@ async function blockProposedEventHandler(data, ivks, nsks) { ); } } catch (err) { - logger.info(err); - logger.info("This encrypted message isn't for this recipient"); + // This error will be caught regularly if the commitment isn't for us + // We dont print anything in order not to pollute the logs } }); } diff --git a/wallet/src/nightfall-browser/services/commitment-storage.js b/wallet/src/nightfall-browser/services/commitment-storage.js index 047cdeaf6..5e13c4fe7 100644 --- a/wallet/src/nightfall-browser/services/commitment-storage.js +++ b/wallet/src/nightfall-browser/services/commitment-storage.js @@ -40,11 +40,12 @@ const connectDB = async () => { }; // function to format a commitment for a mongo db and store it -export async function storeCommitment(commitment, nsk) { - const nullifierHash = new Nullifier(commitment, nsk).hash.hex(32); +export async function storeCommitment(commitment, nullifierKey) { + const nullifierHash = new Nullifier(commitment, nullifierKey).hash.hex(32); const data = { _id: commitment.hash.hex(32), preimage: commitment.preimage.all.hex(32), + compressedZkpPublicKey: commitment.compressedZkpPublicKey.hex(32), isDeposited: commitment.isDeposited || false, isOnChain: Number(commitment.isOnChain) || -1, isPendingNullification: false, // will not be pending when stored @@ -86,7 +87,7 @@ export async function countWithdrawTransactionHashes(transactionHashes) { const db = await connectDB(); const txs = await db.getAll(COMMITMENTS_COLLECTION); const filtered = txs.filter(tx => { - return transactionHashes.includes(tx.transactionHash) && tx.nullifierTransactionType === '3'; + return transactionHashes.includes(tx.transactionHash) && tx.nullifierTransactionType === '2'; }); // const filtered = res.filter(r => transactionHashes.includes(r.transactionHash)); return filtered.length; @@ -97,7 +98,7 @@ export async function isTransactionHashWithdraw(transactionHash) { const db = await connectDB(); const txs = await db.getAll(COMMITMENTS_COLLECTION); const filtered = txs.filter(tx => { - return tx.transactionHash === transactionHash && tx.nullifierTransactionType === '3'; + return tx.transactionHash === transactionHash && tx.nullifierTransactionType === '2'; }); return filtered.length; } @@ -322,12 +323,17 @@ export async function markNullifiedOnChain( } // function to get the balance of commitments for each ERC address -export async function getWalletBalance(pkd) { +export async function getWalletBalance(compressedZkpPublicKey) { const db = await connectDB(); const vals = await db.getAll(COMMITMENTS_COLLECTION); const wallet = Object.keys(vals).length > 0 - ? vals.filter(v => !v.isNullified && v.isOnChain >= 0 && v.compressedPkd === pkd) + ? vals.filter( + v => + !v.isNullified && + v.isOnChain >= 0 && + v.compressedZkpPublicKey === compressedZkpPublicKey, + ) : []; // the below is a little complex. First we extract the ercAddress, tokenId and value // from the preimage. Then we format them nicely. We don't care about the value of the @@ -339,20 +345,21 @@ export async function getWalletBalance(pkd) { return wallet .map(e => ({ ercAddress: `0x${BigInt(e.preimage.ercAddress).toString(16).padStart(40, '0')}`, // Pad this to actual address length - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, tokenId: !!BigInt(e.preimage.tokenId), value: BigInt(e.preimage.value), })) .filter(e => e.tokenId || e.value > 0) // there should be no commitments with tokenId and value of ZERO .map(e => ({ - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, ercAddress: e.ercAddress, balance: e.tokenId ? 1 : e.value, })) .reduce((acc, e) => { - if (!acc[e.compressedPkd]) acc[e.compressedPkd] = {}; - if (!acc[e.compressedPkd][e.ercAddress]) acc[e.compressedPkd][e.ercAddress] = 0n; - acc[e.compressedPkd][e.ercAddress] += e.balance; + if (!acc[e.compressedZkpPublicKey]) acc[e.compressedZkpPublicKey] = {}; + if (!acc[e.compressedZkpPublicKey][e.ercAddress]) + acc[e.compressedZkpPublicKey][e.ercAddress] = 0n; + acc[e.compressedZkpPublicKey][e.ercAddress] += e.balance; return acc; }, {}); } @@ -372,24 +379,24 @@ export async function getWalletPendingDepositBalance() { // work out the balance contribution of each commitment - a 721 token has no value field in the // commitment but each 721 token counts as a balance of 1. Then finally add up the individual // commitment balances to get a balance for each erc address. - console.log(`Wallet: ${JSON.stringify(wallet)}`); return wallet .map(e => ({ ercAddress: `0x${BigInt(e.preimage.ercAddress).toString(16).padStart(40, '0')}`, // Pad this to actual address length - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, tokenId: !!BigInt(e.preimage.tokenId), value: Number(BigInt(e.preimage.value)), })) .filter(e => e.tokenId || e.value > 0) // there should be no commitments with tokenId and value of ZERO .map(e => ({ - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, ercAddress: e.ercAddress, balance: e.tokenId ? 1 : e.value, })) .reduce((acc, e) => { - if (!acc[e.compressedPkd]) acc[e.compressedPkd] = {}; - if (!acc[e.compressedPkd][e.ercAddress]) acc[e.compressedPkd][e.ercAddress] = 0; - acc[e.compressedPkd][e.ercAddress] += e.balance; + if (!acc[e.compressedZkpPublicKey]) acc[e.compressedZkpPublicKey] = {}; + if (!acc[e.compressedZkpPublicKey][e.ercAddress]) + acc[e.compressedZkpPublicKey][e.ercAddress] = 0; + acc[e.compressedZkpPublicKey][e.ercAddress] += e.balance; return acc; }, {}); } @@ -409,26 +416,27 @@ export async function getWalletPendingSpentBalance() { return wallet .map(e => ({ ercAddress: `0x${BigInt(e.preimage.ercAddress).toString(16).padStart(40, '0')}`, // Pad this to actual address length - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, tokenId: !!BigInt(e.preimage.tokenId), value: Number(BigInt(e.preimage.value)), })) .filter(e => e.tokenId || e.value > 0) // there should be no commitments with tokenId and value of ZERO .map(e => ({ - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, ercAddress: e.ercAddress, balance: e.tokenId ? 1 : e.value, })) .reduce((acc, e) => { - if (!acc[e.compressedPkd]) acc[e.compressedPkd] = {}; - if (!acc[e.compressedPkd][e.ercAddress]) acc[e.compressedPkd][e.ercAddress] = 0; - acc[e.compressedPkd][e.ercAddress] += e.balance; + if (!acc[e.compressedZkpPublicKey]) acc[e.compressedZkpPublicKey] = {}; + if (!acc[e.compressedZkpPublicKey][e.ercAddress]) + acc[e.compressedZkpPublicKey][e.ercAddress] = 0; + acc[e.compressedZkpPublicKey][e.ercAddress] += e.balance; return acc; }, {}); } // function to get the balance of commitments for each ERC address -export async function getWalletBalanceDetails(compressedPkd, ercList) { +export async function getWalletBalanceDetails(compressedZkpPublicKey, ercList) { let ercAddressList = ercList || []; ercAddressList = ercAddressList.map(e => e.toUpperCase()); const db = await connectDB(); @@ -446,7 +454,7 @@ export async function getWalletBalanceDetails(compressedPkd, ercList) { const res = wallet .map(e => ({ ercAddress: `0x${BigInt(e.preimage.ercAddress).toString(16).padStart(40, '0')}`, // Pad this to actual address length - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, tokenId: !!BigInt(e.preimage.tokenId), value: Number(BigInt(e.preimage.value)), id: Number(BigInt(e.preimage.tokenId)), @@ -454,22 +462,26 @@ export async function getWalletBalanceDetails(compressedPkd, ercList) { .filter( e => (e.tokenId || e.value > 0) && - e.compressedPkd === compressedPkd && + e.compressedZkpPublicKey === compressedZkpPublicKey && (ercAddressList.length === 0 || ercAddressList.includes(e.ercAddress.toUpperCase())), ) // there should be no commitments with tokenId and value of ZERO .map(e => ({ - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, ercAddress: e.ercAddress, balance: e.tokenId ? 1 : e.value, tokenId: e.id, })) .reduce((acc, e) => { - if (!acc[e.compressedPkd]) acc[e.compressedPkd] = {}; - if (!acc[e.compressedPkd][e.ercAddress]) acc[e.compressedPkd][e.ercAddress] = []; - if (e.tokenId === 0 && acc[e.compressedPkd][e.ercAddress].length > 0) { - acc[e.compressedPkd][e.ercAddress][0].balance += e.balance; + if (!acc[e.compressedZkpPublicKey]) acc[e.compressedZkpPublicKey] = {}; + if (!acc[e.compressedZkpPublicKey][e.ercAddress]) + acc[e.compressedZkpPublicKey][e.ercAddress] = []; + if (e.tokenId === 0 && acc[e.compressedZkpPublicKey][e.ercAddress].length > 0) { + acc[e.compressedZkpPublicKey][e.ercAddress][0].balance += e.balance; } else { - acc[e.compressedPkd][e.ercAddress].push({ balance: e.balance, tokenId: e.tokenId }); + acc[e.compressedZkpPublicKey][e.ercAddress].push({ + balance: e.balance, + tokenId: e.tokenId, + }); } return acc; }, {}); @@ -477,7 +489,7 @@ export async function getWalletBalanceDetails(compressedPkd, ercList) { return res; } -// function to get the commitments for each ERC address of a pkd +// function to get the commitments for each ERC address of a compressedZkpPublicKey export async function getWalletCommitments() { const db = await connectDB(); const vals = await db.getAll(COMMITMENTS_COLLECTION); @@ -493,31 +505,32 @@ export async function getWalletCommitments() { return wallet .map(e => ({ ercAddress: `0x${BigInt(e.preimage.ercAddress).toString(16).padStart(40, '0')}`, - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, tokenId: !!BigInt(e.preimage.tokenId), value: Number(BigInt(e.preimage.value)), })) .filter(e => e.tokenId || e.value > 0) // there should be no commitments with tokenId and value of ZERO .map(e => ({ - compressedPkd: e.compressedPkd, + compressedZkpPublicKey: e.compressedZkpPublicKey, ercAddress: e.ercAddress, balance: e.tokenId ? 1 : e.value, })) .reduce((acc, e) => { - if (!acc[e.compressedPkd]) acc[e.compressedPkd] = {}; - if (!acc[e.compressedPkd][e.ercAddress]) acc[e.compressedPkd][e.ercAddress] = []; - acc[e.compressedPkd][e.ercAddress].push(e); + if (!acc[e.compressedZkpPublicKey]) acc[e.compressedZkpPublicKey] = {}; + if (!acc[e.compressedZkpPublicKey][e.ercAddress]) + acc[e.compressedZkpPublicKey][e.ercAddress] = []; + acc[e.compressedZkpPublicKey][e.ercAddress].push(e); return acc; }, {}); } -// function to get the withdraw commitments for each ERC address of a pkd +// function to get the withdraw commitments for each ERC address of a compressedZkpPublicKey export async function getWithdrawCommitments() { const db = await connectDB(); const vals = await db.getAll(COMMITMENTS_COLLECTION); const withdraws = Object.keys(vals).length > 0 - ? vals.filter(v => v.isNullified && v.isOnChain >= 0 && v.nullifierTransactionType === '3') + ? vals.filter(v => v.isNullified && v.isOnChain >= 0 && v.nullifierTransactionType === '2') : []; // To check validity we need the withdrawal transaction, the block the transaction is in and all other @@ -533,7 +546,7 @@ export async function getWithdrawCommitments() { block, transactions, index, - compressedPkd: w.compressedPkd, + compressedZkpPublicKey: w.compressedZkpPublicKey, ercAddress: `0x${BigInt(w.preimage.ercAddress).toString(16).padStart(40, '0')}`, // Pad this to be a correct address length balance: w.preimage.tokenId ? 1 : w.preimage.value, }; @@ -547,7 +560,7 @@ export async function getWithdrawCommitments() { // TODO isValidWithdrawal is called with wrong parameters const valid = await isValidWithdrawal(block, transactions, index); return { - compressedPkd: wt.compressedPkd, + compressedZkpPublicKey: wt.compressedZkpPublicKey, ercAddress: wt.ercAddress, balance: wt.balance, valid, @@ -556,9 +569,10 @@ export async function getWithdrawCommitments() { ); return withdrawsDetailsValid.reduce((acc, e) => { - if (!acc[e.compressedPkd]) acc[e.compressedPkd] = {}; - if (!acc[e.compressedPkd][e.ercAddress]) acc[e.compressedPkd][e.ercAddress] = []; - acc[e.compressedPkd][e.ercAddress].push(e); + if (!acc[e.compressedZkpPublicKey]) acc[e.compressedZkpPublicKey] = {}; + if (!acc[e.compressedZkpPublicKey][e.ercAddress]) + acc[e.compressedZkpPublicKey][e.ercAddress] = []; + acc[e.compressedZkpPublicKey][e.ercAddress].push(e); return acc; }, {}); } @@ -584,14 +598,14 @@ export async function getCommitmentsFromBlockNumberL2(blockNumberL2) { // also mark any found commitments as nullified (TODO mark them as un-nullified // if the transaction errors). The mutex lock is in the function // findUsableCommitmentsMutex, which calls this function. -async function findUsableCommitments(compressedPkd, ercAddress, tokenId, _value, onlyOne) { +async function findUsableCommitments(compressedZkpPublicKey, ercAddress, tokenId, _value, onlyOne) { const value = generalise(_value); // sometimes this is sent as a BigInt. // eslint-disable-next-line no-undef const db = await connectDB(); const res = await db.getAll(COMMITMENTS_COLLECTION); const commitmentArray = res.filter( r => - r.compressedPkd === compressedPkd.hex(32) && + r.compressedZkpPublicKey === compressedZkpPublicKey.hex(32) && r.preimage.ercAddress.toLowerCase() === ercAddress.hex(32).toLowerCase() && r.preimage.tokenId === tokenId.hex(32) && !r.isNullified && @@ -604,9 +618,7 @@ async function findUsableCommitments(compressedPkd, ercAddress, tokenId, _value, .filter(commitment => Number(commitment.isOnChain) > Number(-1)) // filters for on chain commitments .map(ct => new Commitment(ct.preimage)); // if we have an exact match, we can do a single-commitment transfer. - console.log(`Looking for ${value.hex(32)}, with ercAddress ${ercAddress.hex(32)}`); const [singleCommitment] = commitments.filter(c => { - console.log(`Commitment: ${c.preimage.value.hex(32)}`); return c.preimage.value.hex(32) === value.hex(32); }); if (singleCommitment) { @@ -614,12 +626,30 @@ async function findUsableCommitments(compressedPkd, ercAddress, tokenId, _value, await markPending(singleCommitment); return [singleCommitment]; } - // If we get here it means that we have not been able to find a single commitment that matches the required value - if (onlyOne || commitments.length < 2) return null; // sometimes we require just one commitment + // If we only want one or there is only 1 commitment - then we should try a single transfer with change + if (onlyOne || commitments.length === 1) { + const valuesGreaterThanTarget = commitments.filter(c => c.preimage.value.bigInt > value.bigInt); // Do intermediary step since reduce has ugly exception + if (valuesGreaterThanTarget.length === 0) return null; + const singleCommitmentWithChange = valuesGreaterThanTarget.reduce((prev, curr) => + prev.preimage.value.bigInt < curr.preimage.value.bigInt ? prev : curr, + ); + return [singleCommitmentWithChange]; + } + // If we get here it means that we have not been able to find a single commitment that satisfies our requirements (onlyOne) + if (commitments.length < 2) return null; // sometimes we require just one commitment + + /* if not, maybe we can do more flexible single or double commitment transfers. The current strategy aims to prioritise reducing the complexity of + the commitment set. I.e. Minimise the size of the commitment set by using smaller commitments while also minimising the creation of + low value commitments (dust). - /* if not, maybe we can do a two-commitment transfer. The current strategy aims to prioritise smaller commitments while also - minimising the creation of low value commitments (dust) + Transaction type in order of priority. (1) Double transfer without change, (2) Double Transfer with change, (3) Single Transfer with change. + Double Transfer Without Change: + 1) Sort all commitments by value + 2) Find candidate pairs of commitments that equal the transfer sum. + 3) Select candidate that uses the smallest commitment as one of the input. + + Double Transfer With Change: 1) Sort all commitments by value 2) Split commitments into two sets based of if their values are less than or greater than the target value. LT & GT respectively. 3) If the sum of the two largest values in set LT is LESS than the target value: @@ -633,6 +663,9 @@ async function findUsableCommitments(compressedPkd, ercAddress, tokenId, _value, iii) If the sum of the commitments at the pointers is greater than the target value, we move pointer rhs to the left. iv) Otherwise, we move pointer lhs to the right. v) The selected commitments are the pair that minimise the change difference. The best case in this scenario is a change difference of -1. + + Single Transfer With Change: + 1) If this is the only commitment and it is greater than the transfer sum. */ // sorting will help with making the search easier @@ -640,6 +673,18 @@ async function findUsableCommitments(compressedPkd, ercAddress, tokenId, _value, Number(a.preimage.value.bigInt - b.preimage.value.bigInt), ); + // Find two commitments that matches the transfer value exactly. Double Transfer With No Change. + let lhs = 0; + let rhs = sortedCommits.length - 1; + while (lhs < rhs) { + const tempSum = sortedCommits[lhs].bigInt + sortedCommits[rhs].bigInt; + // The first valid solution will include the smallest usable commitment in the set. + if (tempSum === value.bigInt) break; + else if (tempSum > value.bigInt) rhs--; + else lhs++; + } + if (lhs < rhs) return [sortedCommits[lhs], sortedCommits[rhs]]; + // Find two commitments are greater than the target. Double Transfer With Change // get all commitments less than the target value const commitsLessThanTargetValue = sortedCommits.filter( s => s.preimage.value.bigInt < value.bigInt, @@ -657,8 +702,8 @@ async function findUsableCommitments(compressedPkd, ercAddress, tokenId, _value, } // If we are here than we can use our commitments less than the target value to sum to greater than the target value - let lhs = 0; - let rhs = commitsLessThanTargetValue.length - 1; + lhs = 0; + rhs = commitsLessThanTargetValue.length - 1; let changeDiff = -Infinity; let commitmentsToUse = null; while (lhs < rhs) { @@ -669,7 +714,7 @@ async function findUsableCommitments(compressedPkd, ercAddress, tokenId, _value, // This value will always be negative, // this is equivalent to tempSum - value.bigInt - commitsLessThanTargetValue[lhs].preimage.value.bigInt const tempChangeDiff = commitsLessThanTargetValue[rhs].preimage.value.bigInt - value.bigInt; - if (tempSum > value.bigInt) { + if (tempSum >= value.bigInt) { if (tempChangeDiff > changeDiff) { // We have a set of commitments that has a lower negative change in our outputs. changeDiff = tempChangeDiff; @@ -682,21 +727,22 @@ async function findUsableCommitments(compressedPkd, ercAddress, tokenId, _value, logger.info( `Found commitments suitable for two-token transfer: ${JSON.stringify(commitmentsToUse)}`, ); + await Promise.all(commitmentsToUse.map(commitment => markPending(commitment))); + return commitmentsToUse; } - await Promise.all(commitmentsToUse.map(commitment => markPending(commitment))); - return commitmentsToUse; + return null; } // mutex for the above function to ensure it only runs with a concurrency of one export async function findUsableCommitmentsMutex( - compressedPkd, + compressedZkpPublicKey, ercAddress, tokenId, _value, onlyOne, ) { return mutex.runExclusive(async () => - findUsableCommitments(compressedPkd, ercAddress, tokenId, _value, onlyOne), + findUsableCommitments(compressedZkpPublicKey, ercAddress, tokenId, _value, onlyOne), ); } diff --git a/wallet/src/nightfall-browser/services/deposit.js b/wallet/src/nightfall-browser/services/deposit.js index 6dbf1bf40..26540f8b3 100644 --- a/wallet/src/nightfall-browser/services/deposit.js +++ b/wallet/src/nightfall-browser/services/deposit.js @@ -12,16 +12,17 @@ import gen from 'general-number'; import { initialize } from 'zokrates-js'; -import rand from '../../common-files/utils/crypto/crypto-random'; +import { randValueLT } from '../../common-files/utils/crypto/crypto-random'; import { getContractInstance } from '../../common-files/utils/contract'; import logger from '../../common-files/utils/logger'; import { Commitment, Transaction } from '../classes/index'; import { storeCommitment } from './commitment-storage'; -import { compressPublicKey } from './keys'; +import { ZkpKeys } from './keys'; import { checkIndexDBForCircuit, getStoreCircuit } from './database'; +import { computeWitness } from '../utils/compute-witness'; const { BN128_GROUP_ORDER, USE_STUBS } = global.config; -const { ZKP_KEY_LENGTH, SHIELD_CONTRACT_NAME } = global.nightfallConstants; +const { SHIELD_CONTRACT_NAME } = global.nightfallConstants; const { generalise } = gen; const circuitName = USE_STUBS ? 'deposit_stub' : 'deposit'; @@ -29,8 +30,9 @@ async function deposit(items, shieldContractAddress) { logger.info('Creating a deposit transaction'); // before we do anything else, long hex strings should be generalised to make // subsequent manipulations easier - const { ercAddress, tokenId, value, pkd, nsk, fee } = generalise(items); - const compressedPkd = compressPublicKey(pkd); + const { ercAddress, tokenId, value, compressedZkpPublicKey, nullifierKey, fee } = + generalise(items); + const zkpPublicKey = ZkpKeys.decompressZkpPublicKey(compressedZkpPublicKey); if (!(await checkIndexDBForCircuit(circuitName))) throw Error('Some circuit data are missing from IndexedDB'); @@ -44,27 +46,26 @@ async function deposit(items, shieldContractAddress) { const program = programData.data; const pk = pkData.data; - let commitment; - let salt; - do { - // we also need a salt to make the commitment unique and increase its entropy - // eslint-disable-next-line - salt = await rand(ZKP_KEY_LENGTH); - // next, let's compute the zkp commitment we're going to store and the hash of the public inputs (truncated to 248 bits) - commitment = new Commitment({ ercAddress, tokenId, value, compressedPkd, salt }); - } while (commitment.hash.bigInt > BN128_GROUP_ORDER); - + const salt = await randValueLT(BN128_GROUP_ORDER); + const commitment = new Commitment({ ercAddress, tokenId, value, zkpPublicKey, salt }); logger.debug(`Hash of new commitment is ${commitment.hash.hex()}`); // now we can compute a Witness so that we can generate the proof - const witnessInput = [ - ercAddress.integer, - tokenId.integer, - value.integer, - compressedPkd.limbs(32, 8), - salt.limbs(32, 8), - commitment.hash.integer, - ]; - logger.debug(`witness input is ${witnessInput.join(' ')}`); + const publicData = Transaction.buildSolidityStruct( + new Transaction({ + fee, + transactionType: 0, + tokenType: items.tokenType, + tokenId, + value, + ercAddress, + commitments: [commitment], + }), + ); + + const privateData = { salt, recipientPublicKeys: [zkpPublicKey] }; + const roots = []; + + const witnessInput = computeWitness(publicData, roots, privateData); try { const zokratesProvider = await initialize(); @@ -72,8 +73,10 @@ async function deposit(items, shieldContractAddress) { const keypair = { pk: new Uint8Array(pk) }; // computation + console.log('Computing Witness'); const { witness } = zokratesProvider.computeWitness(artifacts, witnessInput); // generate proof + console.log('Generate Proof'); const { proof } = zokratesProvider.generateProof(artifacts.program, witness, keypair.pk); const shieldContractInstance = await getContractInstance( SHIELD_CONTRACT_NAME, @@ -101,7 +104,7 @@ async function deposit(items, shieldContractAddress) { // store the commitment on successful computation of the transaction commitment.isDeposited = true; - await storeCommitment(commitment, nsk); + await storeCommitment(commitment, nullifierKey); // await saveTransaction(optimisticDepositTransaction); return { rawTransaction, transaction: optimisticDepositTransaction }; } catch (err) { diff --git a/wallet/src/nightfall-browser/services/fetch-circuit.js b/wallet/src/nightfall-browser/services/fetch-circuit.js index 59078a6b4..0ffdee96d 100644 --- a/wallet/src/nightfall-browser/services/fetch-circuit.js +++ b/wallet/src/nightfall-browser/services/fetch-circuit.js @@ -20,7 +20,9 @@ export default async function fetchCircuit( ) { let { abi, program, pk } = circuitFiles[circuit]; // keys path in bucket if (isLocalRun) { - abi = await fetch(`${utilApiServerUrl}/${circuit}/abi.json`).then(response => response.json()); + abi = await fetch(`${utilApiServerUrl}/${circuit}/${circuit}_abi.json`).then(response => + response.json(), + ); program = await fetch(`${utilApiServerUrl}/${circuit}/${circuit}_out`) .then(response => response.body.getReader()) .then(parseData) diff --git a/wallet/src/nightfall-browser/services/kem-dem.ts b/wallet/src/nightfall-browser/services/kem-dem.ts new file mode 100644 index 000000000..2430ed431 --- /dev/null +++ b/wallet/src/nightfall-browser/services/kem-dem.ts @@ -0,0 +1,149 @@ +import gen, { GeneralNumber } from 'general-number'; +import { scalarMult } from '../../common-files/utils/curve-maths/curves'; +import { randValueLT } from '../../common-files/utils/crypto/crypto-random'; +import poseidon from '../../common-files/utils/crypto/poseidon/poseidon'; + +const { BABYJUBJUB, BN128_GROUP_ORDER } = global.config; +const { generalise, stitchLimbs } = gen; +// DOMAIN_KEM = field(SHA256('nightfall-kem')) +const DOMAIN_KEM = 21033365405711675223813179268586447041622169155539365736392974498519442361181n; +// DOMAIN_KEM = field(SHA256('nightfall-dem')) +const DOMAIN_DEM = 1241463701002173366467794894814691939898321302682516549591039420117995599097n; + +/** +This function is like splice but replaces the element in index and returns a new array +@function immutableSplice +@param {Array} array - The list that will be spliced into +@param {number} index - The index to be spliced into +@param {any} element - The element to be placed in index +@returns {Array} An updated array with element spliced in at index. +*/ +const immutableSplice = (array: any, index: number, element: any) => { + const safeIndex = Math.max(index, 0); + const beforeElement = array.slice(0, safeIndex); + const afterElement = array.slice(safeIndex + 1); + return [...beforeElement, element, ...afterElement]; +}; + +/** +This helper function moves the top most 32-bits from one general number to another +@function packSecrets +@param {GeneralNumber} from - The general number which the top more 32-bits will be taken from +@param {GeneralNumber} to - The general number which the top more 32-bits will be inserted into +@param {number} topMostFromBytesIndex - Index to move custom bit positions from +@param {number} topMostToBytesIndex - Index to move custom bit positions to +@returns {Array} The two general numbers after moving the 32-bits. +*/ +const packSecrets = ( + from: GeneralNumber, + to: GeneralNumber, + topMostFromBytesIndex: number, + topMostToBytesIndex: number, +): [GeneralNumber, GeneralNumber] => { + if (topMostFromBytesIndex >= 8 || topMostToBytesIndex >= 8) + throw new Error('This function packs u32[8], indices must be < 8'); + const fromLimbs = from.limbs(32, 8); + const toLimbs = to.limbs(32, 8); + if (toLimbs[0] !== '0') throw new Error('Cannot pack since top bits non-zero'); + + if (topMostToBytesIndex + 1 === toLimbs.length) + throw new Error('Pack To Array is zero, need to specify to address'); + const topMostBytes = fromLimbs[topMostFromBytesIndex]; + + const unpackedFrom = immutableSplice(fromLimbs, topMostFromBytesIndex, '0'); + const packedTo = immutableSplice(toLimbs, topMostToBytesIndex, topMostBytes); + return [generalise(stitchLimbs(unpackedFrom, 32)), generalise(stitchLimbs(packedTo, 32))]; +}; + +/** +This function generates the ephemeral key pair used in the kem-dem +@function genEphemeralKeys +@returns {Promise>>} The private and public key pair +*/ +const genEphemeralKeys = async (): Promise<[GeneralNumber, BigInt[]]> => { + const privateKey: any = await randValueLT(BN128_GROUP_ORDER); + const publicKey = scalarMult(privateKey.bigInt, BABYJUBJUB.GENERATOR); + return [privateKey, publicKey]; +}; + +/** +This function performs the key encapsulation step, deriving a symmetric encryption key from a shared secret. +@function kem +@param {GeneralNumber} privateKey - The private key related to either the ephemeralPub or recipientPubKey (depending on operation) +@param {Array} recipientPubKey - The recipientPubKey, in decryption this is also the ephemeralPub +@returns {Array, BigInt>>} The ephemeralPub key and the symmteric key used for encryption +*/ +const kem = (privateKey: any, recipientPubKey: any) => { + const sharedSecret = scalarMult( + privateKey.bigInt, + recipientPubKey.map((r: any) => r.bigInt), + ); + return poseidon(generalise([sharedSecret[0], sharedSecret[1], DOMAIN_KEM])).bigInt; +}; + +/** +This function performs the data encapsulation step, encrypting the plaintext +@function dem +@param {BigInt} encryptionKey - The symmetric encryption key +@param {Array} plaintexts - The array of plain text to be encrypted +@returns {Array} The encrypted ciphertexts. +*/ +const dem = (encryptionKey: bigint, plaintexts: bigint[]): bigint[] => + plaintexts.map( + (p, i) => + (poseidon(generalise([encryptionKey, DOMAIN_DEM, BigInt(i)])).bigInt + p) % + (BN128_GROUP_ORDER as bigint), + ); + +/** +This function inverts the data encapsulation step, decrypting the ciphertext +@function deDem +@param {BigInt} encryptionKey - The symmetric encryption key +@param {Array} ciphertexts - The array of ciphertexts to be decrypted +@returns {Array} The decrypted plaintexts. +*/ +const deDem = (encryptionKey: bigint, ciphertexts: any) => { + const plainTexts = ciphertexts.map((c: any, i: number) => { + const pt = c.bigInt - poseidon(generalise([encryptionKey, DOMAIN_DEM, BigInt(i)])).bigInt; + if (pt < 0) return ((pt % BN128_GROUP_ORDER) + BN128_GROUP_ORDER) % BN128_GROUP_ORDER; + return pt % BN128_GROUP_ORDER; + }); + return plainTexts; +}; + +/** +This function performs the kem-dem required to encrypt plaintext. +@function encrypt +@param {GeneralNumber} ephemeralPrivate - The private key that generates the ephemeralPub +@param {Array} ephemeralPub - The ephemeralPubKey +@param {Array} recipientPublicKey - The public recipientPublicKey of the recipients +@param {Array} plaintexts - The array of plain text to be encrypted, the ordering is [ercAddress,tokenId, value, salt] +@returns {Array} The encrypted ciphertexts. +*/ +const encrypt = ( + ephemeralPrivate: GeneralNumber, + recipientPublicKey: GeneralNumber[], + plaintexts: bigint[], +): bigint[] => { + const encKey = kem(ephemeralPrivate, recipientPublicKey); + return dem(encKey, plaintexts); +}; + +/** +This function performs the kem-deDem required to decrypt plaintext. +@function decrypt +@param {GeneralNumber} privateKey - The private key of the recipientPublicKey +@param {Array} ephemeralPub - The ephemeralPubKey +@param {Array} cipherTexts - The array of ciphertexts to be decrypted +@returns {Array} The decrypted plaintexts, the ordering is [ercAddress,tokenId, value, salt] +*/ +const decrypt = ( + privateKey: GeneralNumber, + ephemeralPub: GeneralNumber[], + cipherTexts: GeneralNumber[], +): GeneralNumber[] => { + const encKey = kem(privateKey, ephemeralPub); + return deDem(encKey, cipherTexts); +}; + +export { encrypt, decrypt, genEphemeralKeys, packSecrets }; diff --git a/wallet/src/nightfall-browser/services/keys.js b/wallet/src/nightfall-browser/services/keys.js deleted file mode 100644 index 929bc63aa..000000000 --- a/wallet/src/nightfall-browser/services/keys.js +++ /dev/null @@ -1,89 +0,0 @@ -/* ignore unused exports */ - -import { GN, generalise } from 'general-number'; -import { validateMnemonic, mnemonicToSeed } from 'bip39'; -import { hdkey } from 'ethereumjs-wallet'; - -import mimcHash from '../../common-files/utils/crypto/mimc/mimc'; -import { scalarMult, edwardsCompress, edwardsDecompress } from '../utils/crypto/encryption/elgamal'; - -export const ivks = []; -export const nsks = []; -const { BABYJUBJUB, BN128_GROUP_ORDER } = global.config; - -function generateHexSeed(mnemonic) { - return mnemonicToSeed(mnemonic); -} - -// generate key from a seed based on path -function generatePrivateKey(seed, path) { - return ( - new GN(hdkey.fromMasterSeed(seed).derivePath(path).getWallet().getPrivateKey()).bigInt % - BN128_GROUP_ORDER - ); -} - -function calculatePublicKey(privateKey) { - return generalise(scalarMult(privateKey.hex(), BABYJUBJUB.GENERATOR)); -} - -export function compressPublicKey(publicKey) { - return new GN(edwardsCompress([publicKey[0].bigInt, publicKey[1].bigInt])); -} - -export function decompressKey(key) { - return generalise(edwardsDecompress(key.bigInt)); -} - -// path structure is m / purpose' / coin_type' / account' / change / address_index -// the path we use is m/44'/60'/account'/0/address_index -// address will change incrementally for every new set of keys to be created from the same seed -// address_index will define if the key is ask or nsk -// path for ask is m/44'/60'/account'/0/0 -// path for nsk is m/44'/60'/account'/0/1 -async function generateASKAndNSK(seed, path) { - const ask = generalise(generatePrivateKey(seed, `${path}/0`), 'bigInt'); - const nsk = generalise(generatePrivateKey(seed, `${path}/1`), 'bigInt'); - return { ask, nsk }; -} - -// Calculate ivk from ask and nsk such as ivk = MiMC(ask, nsk) -function calculateIVK(ask, nsk) { - return new GN(mimcHash([ask.bigInt, nsk.bigInt])); -} - -// Calculate pkd from ivk such as pkd = ivk.G -export function calculatePkd(ivk) { - const pkd = calculatePublicKey(ivk); - const compressedPkd = compressPublicKey(pkd); - return { pkd, compressedPkd }; -} - -// function to generate all the required keys deterministically from a random mnemonic -// Use mnemonic to generate seed which will then be used to generate sets of ask and nsk based on different account numbers -export async function generateKeys(mnemonic, path) { - if (validateMnemonic(mnemonic)) { - const seed = (await generateHexSeed(mnemonic)).toString('hex'); - const { ask, nsk } = await generateASKAndNSK(seed, path); - const ivk = calculateIVK(ask, nsk); - const { pkd, compressedPkd } = calculatePkd(ivk); - return { - ask: ask.hex(), - nsk: nsk.hex(), - ivk: ivk.hex(), - pkd: [pkd[0].hex(), pkd[1].hex()], - compressedPkd: compressedPkd.hex(), - }; - } - throw new Error('invalid mnemonic'); -} - -export function storeMemoryKeysForDecryption(ivk, nsk) { - return Promise.all([ivks.push(...ivk), nsks.push(...nsk)]); -} - -export function calculateIvkPkdfromAskNsk(ask, nsk) { - const ivk = calculateIVK(ask, nsk); - const { pkd, compressedPkd } = calculatePkd(ivk); - return { ivk, pkd, compressedPkd }; -} diff --git a/wallet/src/nightfall-browser/services/keys.ts b/wallet/src/nightfall-browser/services/keys.ts new file mode 100644 index 000000000..7ed54751d --- /dev/null +++ b/wallet/src/nightfall-browser/services/keys.ts @@ -0,0 +1,117 @@ +/* ignore unused exports */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { GN, generalise, GeneralNumber } from 'general-number'; +import { validateMnemonic, mnemonicToSeedSync } from 'bip39'; +import { hdkey } from 'ethereumjs-wallet'; + +import poseidon from '../../common-files/utils/crypto/poseidon/poseidon'; +import { + scalarMult, + edwardsCompress, + edwardsDecompress, +} from '../../common-files/utils/curve-maths/curves'; + +export const zkpPrivateKeys: any = []; +export const nullifierKeys: any = []; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const { BABYJUBJUB, BN128_GROUP_ORDER } = global.config; + +export class ZkpKeys { + rootKey; + + zkpPrivateKey; + + nullifierKey; + + zkpPublicKey; + + compressedZkpPublicKey; + + constructor(rootKey: GeneralNumber) { + this.rootKey = rootKey; + this.zkpPrivateKey = poseidon([ + rootKey, + new GN(2708019456231621178814538244712057499818649907582893776052749473028258908910n), + ]); + this.nullifierKey = poseidon([ + rootKey, + new GN(7805187439118198468809896822299973897593108379494079213870562208229492109015n), + ]); + const scalarResult: string[] = scalarMult(this.zkpPrivateKey.hex(32), BABYJUBJUB.GENERATOR); + this.zkpPublicKey = generalise(scalarResult); + this.compressedZkpPublicKey = new GN( + edwardsCompress([this.zkpPublicKey[0].bigInt, this.zkpPublicKey[1].bigInt]), + ); + } + + // path structure is m / purpose' / coin_type' / account' / change / address_index + // the path we use is m/44'/60'/account'/0/address_index. 44' is hardened. 60 is Ether. + // change is 0 when external and 1 when internal. External when the public keys will be communicated externally for use + // account will remain 0 and multiple addresses will be created for these keys by incrementing address_index + // path for zkpPrivateKey is m/44'/60'/account'/0/addressIndex + + // function to generate all the required keys deterministically from a random mnemonic + // Use mnemonic to generate seed which will then be used to generate sets of zkpPrivateKey and nullifierKey based on different account numbers + // The domain numbers are derived thusly: + // keccak256('zkpPrivateKey') % BN128_GROUP_ORDER 2708019456231621178814538244712057499818649907582893776052749473028258908910 + // keccak256('nullifierKey') % BN128_GROUP_ORDER 7805187439118198468809896822299973897593108379494079213870562208229492109015 + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + static generateZkpKeysFromMnemonic(mnemonic: string, addressIndex: number) { + if (validateMnemonic(mnemonic)) { + const seed = mnemonicToSeedSync(mnemonic); + const rootKey = generalise( + new GN( + hdkey + .fromMasterSeed(seed) + .derivePath(`m/44'/60'/0'/0/${addressIndex}`) + .getWallet() + .getPrivateKey(), + ).bigInt % BN128_GROUP_ORDER, + ); + const zkpPrivateKey = poseidon([ + rootKey, + new GN(2708019456231621178814538244712057499818649907582893776052749473028258908910n), + ]); + const nullifierKey = poseidon([ + rootKey, + new GN(7805187439118198468809896822299973897593108379494079213870562208229492109015n), + ]); + const scalarResult: string[] = scalarMult(zkpPrivateKey.hex(32), BABYJUBJUB.GENERATOR); + const zkpPublicKey = generalise(scalarResult); + const compressedZkpPublicKey = new GN( + edwardsCompress([zkpPublicKey[0].bigInt, zkpPublicKey[1].bigInt]), + ); + return { + rootKey: generalise(rootKey.field(BN128_GROUP_ORDER)).hex(32), + zkpPrivateKey: zkpPrivateKey.hex(32), + nullifierKey: nullifierKey.hex(32), + zkpPublicKey: [zkpPublicKey[0].hex(32), zkpPublicKey[1].hex(32)], + compressedZkpPublicKey: compressedZkpPublicKey.hex(), + }; + } + throw new Error('invalid mnemonic'); + } + + static calculateZkpPublicKey(zkpPrivateKey: GeneralNumber): { + zkpPublicKey: GeneralNumber[]; + compressedZkpPublicKey: GeneralNumber; + } { + const scalarResult: string[] = scalarMult(zkpPrivateKey.hex(32), BABYJUBJUB.GENERATOR); + const zkpPublicKey = generalise(scalarResult); + const compressedZkpPublicKey = new GN( + edwardsCompress([zkpPublicKey[0].bigInt, zkpPublicKey[1].bigInt]), + ); + return { zkpPublicKey, compressedZkpPublicKey }; + } + + static decompressZkpPublicKey(compressedZkpPublicKey: GeneralNumber): GeneralNumber[] { + const decompressedPoint: string[] = edwardsDecompress(compressedZkpPublicKey.bigInt); + return generalise(decompressedPoint); + } + + static compressZkpPublicKey(zkpPublicKey: GeneralNumber[]): GeneralNumber { + return new GN(edwardsCompress([zkpPublicKey[0].bigInt, zkpPublicKey[1].bigInt])); + } +} diff --git a/wallet/src/nightfall-browser/services/transfer.js b/wallet/src/nightfall-browser/services/transfer.js index 15ac47511..7f03c38ad 100644 --- a/wallet/src/nightfall-browser/services/transfer.js +++ b/wallet/src/nightfall-browser/services/transfer.js @@ -11,10 +11,11 @@ It is agnostic to whether we are dealing with an ERC20 or ERC721 (or ERC1155). import gen from 'general-number'; import { initialize } from 'zokrates-js'; -import rand from '../../common-files/utils/crypto/crypto-random'; +import { randValueLT } from '../../common-files/utils/crypto/crypto-random'; import { getContractInstance } from '../../common-files/utils/contract'; import logger from '../../common-files/utils/logger'; -import { Secrets, Nullifier, Commitment, Transaction } from '../classes/index'; +import { Nullifier, Commitment, Transaction } from '../classes/index'; +import { edwardsCompress } from '../../common-files/utils/curve-maths/curves'; import { findUsableCommitmentsMutex, storeCommitment, @@ -22,32 +23,35 @@ import { clearPending, getSiblingInfo, } from './commitment-storage'; -import { decompressKey, calculateIvkPkdfromAskNsk } from './keys'; +import { ZkpKeys } from './keys'; import { checkIndexDBForCircuit, getStoreCircuit } from './database'; +import { encrypt, genEphemeralKeys, packSecrets } from './kem-dem'; +import { computeWitness } from '../utils/compute-witness'; const { BN128_GROUP_ORDER, USE_STUBS } = global.config; -const { ZKP_KEY_LENGTH, SHIELD_CONTRACT_NAME, ZERO } = global.nightfallConstants; +const { SHIELD_CONTRACT_NAME } = global.nightfallConstants; const { generalise, GN } = gen; -const singleTransfer = USE_STUBS ? 'single_transfer_stub' : 'single_transfer'; -const doubleTransfer = USE_STUBS ? 'double_transfer_stub' : 'double_transfer'; +const circuitName = USE_STUBS ? 'transfer_stub' : 'transfer'; async function transfer(transferParams, shieldContractAddress) { logger.info('Creating a transfer transaction'); // let's extract the input items const { ...items } = transferParams; - const { ercAddress, tokenId, recipientData, nsk, ask, fee } = generalise(items); - const { pkd, compressedPkd } = calculateIvkPkdfromAskNsk(ask, nsk); - const { recipientCompressedPkds, values } = recipientData; - const recipientPkds = recipientCompressedPkds.map(key => decompressKey(key)); - if (recipientCompressedPkds.length > 1) + const { ercAddress, tokenId, recipientData, rootKey, fee } = generalise(items); + const { zkpPublicKey, compressedZkpPublicKey, nullifierKey } = new ZkpKeys(rootKey); + const { recipientCompressedZkpPublicKeys, values } = recipientData; + const recipientZkpPublicKeys = recipientCompressedZkpPublicKeys.map(key => + ZkpKeys.decompressZkpPublicKey(key), + ); + if (recipientCompressedZkpPublicKeys.length > 1) throw new Error(`Batching is not supported yet: only one recipient is allowed`); // this will not always be true so we try to make the following code agnostic to the number of commitments // the first thing we need to do is to find some input commitments which // will enable us to conduct our transfer. Let's rummage in the db... const totalValueToSend = values.reduce((acc, value) => acc + value.bigInt, 0n); const oldCommitments = await findUsableCommitmentsMutex( - compressedPkd, + compressedZkpPublicKey, ercAddress, tokenId, totalValueToSend, @@ -57,7 +61,7 @@ async function transfer(transferParams, shieldContractAddress) { try { // Having found either 1 or 2 commitments, which are suitable inputs to the // proof, the next step is to compute their nullifiers; - const nullifiers = oldCommitments.map(commitment => new Nullifier(commitment, nsk)); + const nullifiers = oldCommitments.map(commitment => new Nullifier(commitment, nullifierKey)); // then the new output commitment(s) const totalInputCommitmentValue = oldCommitments.reduce( (acc, commitment) => acc + commitment.preimage.value.bigInt, @@ -68,43 +72,36 @@ async function transfer(transferParams, shieldContractAddress) { // if so, add an output commitment to do that if (change !== 0n) { values.push(new GN(change)); - recipientPkds.push(pkd); - recipientCompressedPkds.push(compressedPkd); + recipientZkpPublicKeys.push(zkpPublicKey); + recipientCompressedZkpPublicKeys.push(compressedZkpPublicKey); } - const newCommitments = []; - let secrets = []; - const salts = []; - let potentialSalt; - let potentialCommitment; - for (let i = 0; i < recipientCompressedPkds.length; i++) { - // loop to find a new salt until the commitment hash is smaller than the BN128_GROUP_ORDER - do { - // eslint-disable-next-line no-await-in-loop - potentialSalt = new GN((await rand(ZKP_KEY_LENGTH)).bigInt % BN128_GROUP_ORDER); - potentialCommitment = new Commitment({ + // Generate salts, constrained to be < field size + const salts = await Promise.all(values.map(async () => randValueLT(BN128_GROUP_ORDER))); + const newCommitments = values.map( + (value, i) => + new Commitment({ ercAddress, tokenId, - value: values[i], - pkd: recipientPkds[i], - compressedPkd: recipientCompressedPkds[i], - salt: potentialSalt, - }); - // encrypt secrets such as erc20Address, tokenId, value, salt for recipient - if (i === 0) { - // eslint-disable-next-line no-await-in-loop - secrets = await Secrets.encryptSecrets( - [ercAddress.bigInt, tokenId.bigInt, values[i].bigInt, potentialSalt.bigInt], - [recipientPkds[0][0].bigInt, recipientPkds[0][1].bigInt], - ); - } - } while (potentialCommitment.hash.bigInt > BN128_GROUP_ORDER); - salts.push(potentialSalt); - newCommitments.push(potentialCommitment); - } + value, + zkpPublicKey: recipientZkpPublicKeys[i], + salt: salts[i].bigInt, + }), + ); - // compress the secrets to save gas - const compressedSecrets = Secrets.compressSecrets(secrets); + // KEM-DEM encryption + const [ePrivate, ePublic] = await genEphemeralKeys(); + const [unpackedTokenID, packedErc] = packSecrets(tokenId, ercAddress, 0, 2); + const compressedSecrets = encrypt(generalise(ePrivate), generalise(recipientZkpPublicKeys[0]), [ + packedErc.bigInt, + unpackedTokenID.bigInt, + values[0].bigInt, + salts[0].bigInt, + ]); + // Compress the public key as it will be put on-chain + const compressedEPub = edwardsCompress(ePublic); + + // Commitment Tree Information const commitmentTreeInfo = await Promise.all(oldCommitments.map(c => getSiblingInfo(c))); const localSiblingPaths = commitmentTreeInfo.map(l => { const path = l.siblingPath.path.map(p => p.value); @@ -112,12 +109,16 @@ async function transfer(transferParams, shieldContractAddress) { }); const leafIndices = commitmentTreeInfo.map(l => l.leafIndex); const blockNumberL2s = commitmentTreeInfo.map(l => l.isOnChain); + const roots = commitmentTreeInfo.map(l => l.root); + logger.info( + 'Constructing transfer transaction with blockNumberL2s', + blockNumberL2s, + 'and roots', + roots, + ); // time for a quick sanity check. We expect the number of old commitments, // new commitments and nullifiers to be equal. - if ( - nullifiers.length !== oldCommitments.length || - nullifiers.length !== newCommitments.length - ) { + if (nullifiers.length !== oldCommitments.length) { logger.error( `number of old commitments: ${oldCommitments.length}, number of new commitments: ${newCommitments.length}, number of nullifiers: ${nullifiers.length}`, ); @@ -127,107 +128,61 @@ async function transfer(transferParams, shieldContractAddress) { } // now we have everything we need to create a Witness and compute a proof - const witnessInput = [ - oldCommitments.map(commitment => commitment.preimage.ercAddress.integer).flat(), - oldCommitments.map(commitment => { - return { - id: commitment.preimage.tokenId.limbs(32, 8), - value: commitment.preimage.value.limbs(32, 8), - salt: commitment.preimage.salt.limbs(32, 8), - hash: commitment.hash.limbs(32, 8), - ask: ask.field(BN128_GROUP_ORDER), - }; + const transaction = Transaction.buildSolidityStruct( + new Transaction({ + fee, + historicRootBlockNumberL2: blockNumberL2s, + transactionType: 1, + ercAddress: compressedSecrets[0], // this is the encrypted ercAddress + tokenId: compressedSecrets[1], // this is the encrypted tokenID + recipientAddress: compressedEPub, + commitments: newCommitments, + nullifiers, + compressedSecrets: compressedSecrets.slice(2), // these are the [value, salt] }), - newCommitments.map(commitment => { - return { - pkdRecipient: [ - commitment.preimage.pkd[0].field(BN128_GROUP_ORDER), - commitment.preimage.pkd[1].field(BN128_GROUP_ORDER), - ], - value: commitment.preimage.value.limbs(32, 8), - salt: commitment.preimage.salt.limbs(32, 8), - }; + ); + + const privateData = { + rootKey: [rootKey, rootKey], + oldCommitmentPreimage: oldCommitments.map(o => { + return { value: o.preimage.value, salt: o.preimage.salt }; }), - newCommitments.map(commitment => commitment.hash.integer), - nullifiers.map(nullifier => nullifier.preimage.nsk.limbs(32, 8)), - nullifiers.map(nullifier => generalise(nullifier.hash.hex(32, 31)).integer), - localSiblingPaths.map(siblingPath => siblingPath[0].field(BN128_GROUP_ORDER, false)), - localSiblingPaths.map(siblingPath => - siblingPath.slice(1).map(node => node.field(BN128_GROUP_ORDER, false)), - ), // siblingPAth[32] is a sha hash and will overflow a field but it's ok to take the mod here - hence the 'false' flag - leafIndices.map(leaf => leaf.toString()), - { - ephemeralKey1: secrets.ephemeralKeys[0].limbs(32, 8), - ephemeralKey2: secrets.ephemeralKeys[1].limbs(32, 8), - ephemeralKey3: secrets.ephemeralKeys[2].limbs(32, 8), - ephemeralKey4: secrets.ephemeralKeys[3].limbs(32, 8), - cipherText: secrets.cipherText.flat().map(text => text.field(BN128_GROUP_ORDER)), - sqrtMessage1: secrets.squareRootsElligator2[0].field(BN128_GROUP_ORDER), - sqrtMessage2: secrets.squareRootsElligator2[1].field(BN128_GROUP_ORDER), - sqrtMessage3: secrets.squareRootsElligator2[2].field(BN128_GROUP_ORDER), - sqrtMessage4: secrets.squareRootsElligator2[3].field(BN128_GROUP_ORDER), - }, - compressedSecrets.map(text => { - const bin = text.binary.padStart(256, '0'); - const parity = bin[0]; - const ordinate = bin.slice(1); - const fields = { - parity: !!Number(parity), // This converts parity into true / false from 1 / 0; - ordinate: new GN(ordinate, 'binary').field(BN128_GROUP_ORDER), - }; - return fields; + paths: localSiblingPaths.map(siblingPath => siblingPath.slice(1)), + orders: leafIndices, + newCommitmentPreimage: newCommitments.map(o => { + return { value: o.preimage.value, salt: o.preimage.salt }; }), - ]; + recipientPublicKeys: newCommitments.map(o => o.preimage.zkpPublicKey), + ercAddress, + tokenId, + ephemeralKey: ePrivate, + }; - const flattenInput = witnessInput.map(w => { - if (w.length === 1) { - const [w_] = w; - return w_; - } - return w; - }); + const witnessInput = computeWitness(transaction, roots, privateData); - console.log(`witness input is ${JSON.stringify(flattenInput)}`); // call a zokrates worker to generate the proof // This is (so far) the only place where we need to get specific about the // circuit - let abi; - let program; - let pk; - let transactionType; - if (oldCommitments.length === 1) { - transactionType = 1; - blockNumberL2s.push(0); // We need top pad block numbers if we do a single transfer - if (!(await checkIndexDBForCircuit(singleTransfer))) - throw Error('Some circuit data are missing from IndexedDB'); - const [abiData, programData, pkData] = await Promise.all([ - getStoreCircuit(`${singleTransfer}-abi`), - getStoreCircuit(`${singleTransfer}-program`), - getStoreCircuit(`${singleTransfer}-pk`), - ]); - abi = abiData.data; - program = programData.data; - pk = pkData.data; - } else if (oldCommitments.length === 2) { - transactionType = 2; - if (!(await checkIndexDBForCircuit(doubleTransfer))) - throw Error('Some circuit data are missing from IndexedDB'); - const [abiData, programData, pkData] = await Promise.all([ - getStoreCircuit(`${doubleTransfer}-abi`), - getStoreCircuit(`${doubleTransfer}-program`), - getStoreCircuit(`${doubleTransfer}-pk`), - ]); - abi = abiData.data; - program = programData.data; - pk = pkData.data; - } else throw new Error('Unsupported number of commitments'); + if (!(await checkIndexDBForCircuit(circuitName))) + throw Error('Some circuit data are missing from IndexedDB'); + const [abiData, programData, pkData] = await Promise.all([ + getStoreCircuit(`${circuitName}-abi`), + getStoreCircuit(`${circuitName}-program`), + getStoreCircuit(`${circuitName}-pk`), + ]); + const abi = abiData.data; + const program = programData.data; + const pk = pkData.data; const zokratesProvider = await initialize(); const artifacts = { program: new Uint8Array(program), abi }; const keypair = { pk: new Uint8Array(pk) }; - const { witness } = zokratesProvider.computeWitness(artifacts, flattenInput); + console.log('Computing witness'); + const { witness } = zokratesProvider.computeWitness(artifacts, witnessInput); // generate proof + console.log('Generating Proof'); let { proof } = zokratesProvider.generateProof(artifacts.program, witness, keypair.pk); + console.log('Proof Generated'); proof = [...proof.a, ...proof.b, ...proof.c]; proof = proof.flat(Infinity); // and work out the ABI encoded data that the caller should sign and send to the shield contract @@ -238,45 +193,24 @@ async function transfer(transferParams, shieldContractAddress) { const optimisticTransferTransaction = new Transaction({ fee, historicRootBlockNumberL2: blockNumberL2s, - transactionType, - ercAddress: ZERO, + transactionType: 1, + ercAddress: compressedSecrets[0], // this is the encrypted ercAddress + tokenId: compressedSecrets[1], // this is the encrypted tokenID + recipientAddress: compressedEPub, commitments: newCommitments, nullifiers, - compressedSecrets, + compressedSecrets: compressedSecrets.slice(2), // these are the [value, salt] proof, }); - // if (offchain) { - // await axios - // .post( - // `${proposerUrl}/proposer/offchain-transaction`, - // { transaction: optimisticTransferTransaction }, - // { timeout: 3600000 }, - // ) - // .catch(err => { - // throw new Error(err); - // }); - // // we only want to store our own commitments so filter those that don't - // // have our public key - // newCommitments - // .filter(commitment => commitment.compressedPkd.hex(32) === compressedPkd.hex(32)) - // .forEach(commitment => storeCommitment(commitment, nsk)); // TODO insertMany - // // mark the old commitments as nullified - // await Promise.all( - // oldCommitments.map(commitment => markNullified(commitment, optimisticTransferTransaction)), - // ); - // await saveTransaction(optimisticTransferTransaction); - // return { - // transaction: optimisticTransferTransaction, - // salts: salts.map(salt => salt.hex(32)), - // }; - // } const rawTransaction = await shieldContractInstance.methods .submitTransaction(Transaction.buildSolidityStruct(optimisticTransferTransaction)) .encodeABI(); // store the commitment on successful computation of the transaction newCommitments - .filter(commitment => commitment.compressedPkd.hex(32) === compressedPkd.hex(32)) - .forEach(commitment => storeCommitment(commitment, nsk)); // TODO insertMany + .filter( + commitment => commitment.compressedZkpPublicKey.hex(32) === compressedZkpPublicKey.hex(32), + ) + .forEach(commitment => storeCommitment(commitment, nullifierKey)); // TODO insertMany // mark the old commitments as nullified await Promise.all( oldCommitments.map(commitment => markNullified(commitment, optimisticTransferTransaction)), @@ -289,6 +223,7 @@ async function transfer(transferParams, shieldContractAddress) { }; } catch (err) { await Promise.all(oldCommitments.map(commitment => clearPending(commitment))); + console.log('err', err); throw new Error(err); // let the caller handle the error } } diff --git a/wallet/src/nightfall-browser/services/withdraw.js b/wallet/src/nightfall-browser/services/withdraw.js index c57304e49..75333cb2f 100644 --- a/wallet/src/nightfall-browser/services/withdraw.js +++ b/wallet/src/nightfall-browser/services/withdraw.js @@ -10,28 +10,32 @@ It is agnostic to whether we are dealing with an ERC20 or ERC721 (or ERC1155). import gen from 'general-number'; import { initialize } from 'zokrates-js'; import { getContractInstance } from '../../common-files/utils/contract'; +import { randValueLT } from '../../common-files/utils/crypto/crypto-random'; import logger from '../../common-files/utils/logger'; -import { Nullifier, Transaction } from '../classes/index'; +import { Commitment, Nullifier, Transaction } from '../classes/index'; import { findUsableCommitmentsMutex, markNullified, clearPending, getSiblingInfo, + storeCommitment, } from './commitment-storage'; -import { calculateIvkPkdfromAskNsk } from './keys'; +import { ZkpKeys } from './keys'; +import { computeWitness } from '../utils/compute-witness'; import { checkIndexDBForCircuit, getStoreCircuit } from './database'; const { BN128_GROUP_ORDER, USE_STUBS } = global.config; const { SHIELD_CONTRACT_NAME } = global.nightfallConstants; -const { generalise } = gen; +const { generalise, GN } = gen; const circuitName = USE_STUBS ? 'withdraw_stub' : 'withdraw'; +const MAX_WITHDRAW = 5192296858534827628530496329220096n; // 2n**112n + async function withdraw(withdrawParams, shieldContractAddress) { logger.info('Creating a withdraw transaction'); // let's extract the input items - const { ercAddress, tokenId, value, recipientAddress, nsk, ask, fee } = - generalise(withdrawParams); - const { compressedPkd } = calculateIvkPkdfromAskNsk(ask, nsk); + const { ercAddress, tokenId, value, recipientAddress, rootKey, fee } = generalise(withdrawParams); + const { compressedZkpPublicKey, nullifierKey, zkpPublicKey } = new ZkpKeys(rootKey); if (!(await checkIndexDBForCircuit(circuitName))) throw Error('Some circuit data are missing from IndexedDB'); @@ -47,49 +51,86 @@ async function withdraw(withdrawParams, shieldContractAddress) { // the first thing we need to do is to find and input commitment which // will enable us to conduct our withdraw. Let's rummage in the db... - const [oldCommitment] = (await findUsableCommitmentsMutex( - compressedPkd, + const oldCommitments = await findUsableCommitmentsMutex( + compressedZkpPublicKey, ercAddress, tokenId, value, true, - )) || [null]; - if (oldCommitment) logger.debug(`Found commitment ${JSON.stringify(oldCommitment, null, 2)}`); + ); + if (oldCommitments) logger.debug(`Found commitment ${JSON.stringify(oldCommitments, null, 2)}`); else throw new Error('No suitable commitments were found'); // caller to handle - need to get the user to make some commitments or wait until they've been posted to the blockchain and Timber knows about them try { // Having found 1 commitment, which is a suitable input to the // proof, the next step is to compute its nullifier; - const nullifier = new Nullifier(oldCommitment, nsk); - // and the Merkle path from the commitment to the root - const commitmentTreeInfo = await getSiblingInfo(oldCommitment); - const siblingPath = generalise( - [commitmentTreeInfo.root].concat( - commitmentTreeInfo.siblingPath.path.map(p => p.value).reverse(), - ), + const nullifiers = oldCommitments.map( + oldCommitment => new Nullifier(oldCommitment, nullifierKey), + ); + // we may need to return change to the recipient + const totalInputCommitmentValue = oldCommitments.reduce( + (acc, curr) => curr.preimage.value.bigInt + acc, + 0n, ); + const withdrawValue = value.bigInt > MAX_WITHDRAW ? MAX_WITHDRAW : value.bigInt; + const change = totalInputCommitmentValue - withdrawValue; + // and the Merkle path from the commitment to the root + const commitmentTreeInfo = await Promise.all(oldCommitments.map(c => getSiblingInfo(c))); + const localSiblingPaths = commitmentTreeInfo.map(l => { + const path = l.siblingPath.path.map(p => p.value); + return generalise([l.root].concat(path.reverse())); + }); // public inputs - const { leafIndex, isOnChain } = commitmentTreeInfo; + const leafIndices = commitmentTreeInfo.map(l => l.leafIndex); + const blockNumberL2s = commitmentTreeInfo.map(l => l.isOnChain); + const newCommitment = []; + const salt = await randValueLT(BN128_GROUP_ORDER); + if (change !== 0n) { + newCommitment.push( + new Commitment({ + ercAddress, + tokenId, + value: new GN(change), + zkpPublicKey, + salt: salt.bigInt, + }), + ); + } // now we have everything we need to create a Witness and compute a proof - const witnessInput = [ - oldCommitment.preimage.ercAddress.integer, - oldCommitment.preimage.tokenId.integer, - oldCommitment.preimage.value.integer, - { - salt: oldCommitment.preimage.salt.limbs(32, 8), - hash: oldCommitment.hash.limbs(32, 8), - ask: ask.field(BN128_GROUP_ORDER), - }, - nullifier.preimage.nsk.limbs(32, 8), - generalise(nullifier.hash.hex(32, 31)).integer, - recipientAddress.field(BN128_GROUP_ORDER), - siblingPath[0].field(BN128_GROUP_ORDER), - siblingPath.slice(1).map(node => node.field(BN128_GROUP_ORDER, false)), // siblingPAth[32] is a sha hash and will overflow a field but it's ok to take the mod here - hence the 'false' flag - leafIndex.toString(), - ]; + const publicData = Transaction.buildSolidityStruct( + new Transaction({ + fee, + historicRootBlockNumberL2: blockNumberL2s, + commitments: newCommitment.length > 0 ? newCommitment : [{ hash: 0 }, { hash: 0 }], + transactionType: 2, + tokenType: withdrawParams.tokenType, + tokenId, + value, + ercAddress, + recipientAddress, + nullifiers, + }), + ); + const privateData = { + rootKey: [rootKey, rootKey], + oldCommitmentPreimage: oldCommitments.map(o => { + return { value: o.preimage.value, salt: o.preimage.salt }; + }), + paths: localSiblingPaths.map(siblingPath => siblingPath.slice(1)), + orders: leafIndices, + newCommitmentPreimage: newCommitment.map(o => { + return { value: o.preimage.value, salt: o.preimage.salt }; + }), + recipientPublicKeys: newCommitment.map(o => o.preimage.zkpPublicKey), + }; + + const witnessInput = computeWitness( + publicData, + localSiblingPaths.map(siblingPath => siblingPath[0]), + privateData, + ); - logger.debug(`witness input is ${JSON.stringify(witnessInput)}`); // call a zokrates worker to generate the proof const zokratesProvider = await initialize(); const artifacts = { program: new Uint8Array(program), abi }; @@ -107,25 +148,29 @@ async function withdraw(withdrawParams, shieldContractAddress) { ); const optimisticWithdrawTransaction = new Transaction({ fee, - historicRootBlockNumberL2: [isOnChain, 0], - transactionType: 3, + historicRootBlockNumberL2: blockNumberL2s, + commitments: newCommitment.length > 0 ? newCommitment : [{ hash: 0 }, { hash: 0 }], + transactionType: 2, tokenType: withdrawParams.tokenType, tokenId, value, ercAddress, recipientAddress, - nullifiers: [nullifier], + nullifiers, proof, }); const rawTransaction = await shieldContractInstance.methods .submitTransaction(Transaction.buildSolidityStruct(optimisticWithdrawTransaction)) .encodeABI(); + if (change !== 0n) await storeCommitment(newCommitment[0], nullifierKey); // on successful computation of the transaction mark the old commitments as nullified - await markNullified(oldCommitment, optimisticWithdrawTransaction); + await Promise.all( + oldCommitments.map(commitment => markNullified(commitment, optimisticWithdrawTransaction)), + ); // await saveTransaction(optimisticWithdrawTransaction); return { rawTransaction, transaction: optimisticWithdrawTransaction }; } catch (err) { - await clearPending(oldCommitment); + await Promise.all(oldCommitments.map(commitment => clearPending(commitment))); throw new Error(err); // let the caller handle the error } } diff --git a/wallet/src/nightfall-browser/utils/compute-witness.ts b/wallet/src/nightfall-browser/utils/compute-witness.ts new file mode 100644 index 000000000..17f16d28e --- /dev/null +++ b/wallet/src/nightfall-browser/utils/compute-witness.ts @@ -0,0 +1,172 @@ +import gen, { GeneralNumber } from 'general-number'; + +const { generalise } = gen; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const { BN128_GROUP_ORDER } = global.config; +const NULL_COMMITMENT = { + value: 0, + salt: 0, +}; +const padArray = (arr: T[], padWith: any, n: number): T[] => { + if (!Array.isArray(arr)) + return generalise([arr, ...Array.from({ length: n - 1 }, () => padWith)]); + if (arr.length < n) { + const nullPadding = Array.from({ length: n - arr.length }, () => padWith); + return generalise(arr.concat(nullPadding)); + } + return generalise(arr); +}; + +type PublicInputs = { + value: string; + historicRootBlockNumberL2: string; + transactionType: string; + tokenType: string; + tokenId: string[]; + ercAddress: string; + recipientAddress: string; + commitments: string[]; + nullifiers: string[]; + compressedSecrets: string[]; +}; + +type CommitmentPreimage = { + value: string[][]; + salt: string[]; +}; + +type Nullifier = { + oldCommitments: CommitmentPreimage; + rootKey: string[]; + paths: string[][]; + orders: string[]; +}; + +type Commitment = { + newCommitments: CommitmentPreimage; + recipientPublicKey: string[][]; +}; + +type Transfer = { + ephemeralKey: string[]; + ercAddressTransfer: string[]; + idTransfer: string[]; +}; + +const computePublicInputs = (tx: any, roots: any) => { + const transaction = generalise(tx); + const rootsOldCommitments = padArray(generalise(roots), 0, 2); + const publicInput = []; + const publicTx: PublicInputs = { + value: transaction.value.field(BN128_GROUP_ORDER), + historicRootBlockNumberL2: transaction.historicRootBlockNumberL2.map((h: any) => + h.field(BN128_GROUP_ORDER), + ), + transactionType: transaction.transactionType.field(BN128_GROUP_ORDER), + tokenType: transaction.tokenType.field(BN128_GROUP_ORDER), + tokenId: transaction.tokenId.limbs(32, 8), + ercAddress: transaction.ercAddress.field(BN128_GROUP_ORDER), + recipientAddress: transaction.recipientAddress.limbs(32, 8), + commitments: transaction.commitments.map((c: any) => c.field(BN128_GROUP_ORDER)), + nullifiers: transaction.nullifiers.map((n: any) => n.field(BN128_GROUP_ORDER)), + compressedSecrets: transaction.compressedSecrets.map((cs: any) => cs.field(BN128_GROUP_ORDER)), + }; + publicInput.push(publicTx); + if (Number(tx.transactionType) !== 0) { + publicInput.push(rootsOldCommitments.map((r: any) => r.field(BN128_GROUP_ORDER)).flat()); + } + + return publicInput; +}; + +const computePrivateInputsEncryption = (privateData: any): Transfer => { + const { ephemeralKey, ercAddress, tokenId } = generalise(privateData); + return { + ephemeralKey: ephemeralKey.limbs(32, 8), + ercAddressTransfer: ercAddress.field(BN128_GROUP_ORDER), + idTransfer: tokenId.limbs(32, 8), + }; +}; + +const computePrivateInputsNullifiers = (privateData: any): Nullifier => { + const { oldCommitmentPreimage, paths, orders, rootKey } = generalise(privateData); + const paddedOldCommitmentPreimage: Record[] = padArray( + oldCommitmentPreimage, + NULL_COMMITMENT, + 2, + ); + const paddedPaths: GeneralNumber[][] = padArray(paths, new Array(32).fill(0), 2); + const paddedOrders: GeneralNumber[] = padArray(orders, 0, 2); + const paddedRootKeys: GeneralNumber[] = padArray(rootKey, 0, 2); + + return { + oldCommitments: { + value: paddedOldCommitmentPreimage.map(commitment => commitment.value.limbs(8, 31)), + salt: paddedOldCommitmentPreimage.map(commitment => commitment.salt.field(BN128_GROUP_ORDER)), + }, + rootKey: paddedRootKeys.map(r => r.field(BN128_GROUP_ORDER)), + paths: paddedPaths.map(ps => ps.map(p => p.field(BN128_GROUP_ORDER))), + orders: paddedOrders.map(m => m.field(BN128_GROUP_ORDER)), + }; +}; + +const computePrivateInputsCommitments = (privateData: any, padTo: number): Commitment => { + const { newCommitmentPreimage, recipientPublicKeys } = generalise(privateData); + const paddedNewCommitmentPreimage: Record[] = padArray( + newCommitmentPreimage, + NULL_COMMITMENT, + padTo, + ); + const paddedRecipientPublicKeys: GeneralNumber[][] = padArray(recipientPublicKeys, [0, 0], padTo); + return { + newCommitments: { + value: paddedNewCommitmentPreimage.map(commitment => commitment.value.limbs(8, 31)), + salt: paddedNewCommitmentPreimage.map(commitment => commitment.salt.field(BN128_GROUP_ORDER)), + }, + recipientPublicKey: paddedRecipientPublicKeys.map(rcp => [ + rcp[0].field(BN128_GROUP_ORDER), + rcp[1].field(BN128_GROUP_ORDER), + ]), + }; +}; + +const computePrivateInputsDeposit = (privateData: any): string[] => { + const { salt, recipientPublicKeys } = generalise(privateData); + return [ + salt.field(BN128_GROUP_ORDER), + recipientPublicKeys.map((rcp: GeneralNumber[]) => [ + rcp[0].field(BN128_GROUP_ORDER), + rcp[1].field(BN128_GROUP_ORDER), + ]), + ].flat(1); +}; + +// eslint-disable-next-line import/prefer-default-export +export const computeWitness = ( + txObject: PublicInputs, + roots: any[], + privateData: Record, +): any => { + const publicInputs = computePublicInputs(txObject, roots); + switch (Number(txObject.transactionType)) { + case 0: + // Deposit + return [...publicInputs, ...computePrivateInputsDeposit(privateData)]; + case 1: + // Transfer + return [ + ...publicInputs, + computePrivateInputsNullifiers(privateData), + computePrivateInputsCommitments(privateData, 2), + computePrivateInputsEncryption(privateData), + ]; + default: + // Withdraw + return [ + ...publicInputs, + computePrivateInputsNullifiers(privateData), + computePrivateInputsCommitments(privateData, 1), + ]; + } +}; diff --git a/wallet/src/nightfall-browser/utils/crypto/encryption/elgamal.js b/wallet/src/nightfall-browser/utils/crypto/encryption/elgamal.js deleted file mode 100644 index 24c034cfb..000000000 --- a/wallet/src/nightfall-browser/utils/crypto/encryption/elgamal.js +++ /dev/null @@ -1,211 +0,0 @@ -// ignore unused exports dec, enc, scalarMult, edwardsCompress, edwardsDecompress - -/** -functions to support El-Gamal cipherText over a BabyJubJub curve -*/ - -import utils from '../../../../common-files/utils/crypto/merkle-tree/utils'; -import { - squareRootModPrime, - addMod, - mulMod, -} from '../../../../common-files/utils/crypto/number-theory'; -import modDivide from './modular-division'; // TODO REPLACE WITH NPM VERSION -import { hashToCurve, hashToCurveYSqrt, curveToHash } from './elligator2'; - -const { BABYJUBJUB, BN128_GROUP_ORDER } = global.config; - -const one = BigInt(1); -const { JUBJUBE, JUBJUBC, JUBJUBD, JUBJUBA, GENERATOR } = BABYJUBJUB; -const Fp = BN128_GROUP_ORDER; // the prime field used with the curve E(Fp) -const Fq = JUBJUBE / JUBJUBC; - -function isOnCurve(p) { - const { JUBJUBA: a, JUBJUBD: d } = BABYJUBJUB; - const uu = (p[0] * p[0]) % Fp; - const vv = (p[1] * p[1]) % Fp; - const uuvv = (uu * vv) % Fp; - return (a * uu + vv) % Fp === (one + d * uuvv) % Fp; -} - -// // is On Montgomery curve By^2 = x^3 + Ax^2 + x -// function isOnCurveMF(p) { -// const { MONTA: a, MONTB: b } = BABYJUBJUB; -// const u = p[0]; -// const uu = (p[0] * p[0]) % Fp; -// const uuu = (p[0] * p[0] * p[0]) % Fp; -// const vv = (p[1] * p[1]) % Fp; -// return (b * vv) % Fp === (uuu + a * uu + u) % Fp; -// } - -function negate(g) { - return [Fp - g[0], g[1]]; // this is wierd - we negate the x coordinate, not the y with babyjubjub! -} - -/** -Point addition on the babyjubjub curve TODO - MOD P THIS -*/ -function add(p, q) { - const { JUBJUBA: a, JUBJUBD: d } = BABYJUBJUB; - const u1 = p[0]; - const v1 = p[1]; - const u2 = q[0]; - const v2 = q[1]; - const uOut = modDivide(u1 * v2 + v1 * u2, one + d * u1 * u2 * v1 * v2, Fp); - const vOut = modDivide(v1 * v2 - a * u1 * u2, one - d * u1 * u2 * v1 * v2, Fp); - if (!isOnCurve([uOut, vOut])) throw new Error('Addition point is not on the babyjubjub curve'); - return [uOut, vOut]; -} - -/** -Scalar multiplication on a babyjubjub curve -@param {String} scalar - scalar mod q (will wrap if greater than mod q, which is probably ok) -@param {Object} h - curve point in u,v coordinates -*/ -function scalarMult(scalar, h, form = 'Edwards') { - const { INFINITY } = BABYJUBJUB; - const a = ((BigInt(scalar) % Fq) + Fq) % Fq; // just in case we get a value that's too big or negative - const exponent = a.toString(2).split(''); // extract individual binary elements - let doubledP = [...h]; // shallow copy h to prevent h being mutated by the algorithm - let accumulatedP = INFINITY; - for (let i = exponent.length - 1; i >= 0; i--) { - const candidateP = add(accumulatedP, doubledP, form); - accumulatedP = exponent[i] === '1' ? candidateP : accumulatedP; - doubledP = add(doubledP, doubledP, form); - } - if (!isOnCurve(accumulatedP)) - throw new Error('Scalar multiplication point is not on the babyjubjub curve'); - return accumulatedP; -} - -/** -Converting Montgomery point to Twisted Edwards point -@param {String} p - point in Montgomery form -*/ -function montgomeryToTwistedEdwards(p) { - if (p[0] === BigInt(0) && p[1] === BigInt(0) && p.length === 2) - return [BigInt(0), BigInt(Fp - BigInt(1))]; // M -> T [0,0] -> [0,-1] - const u = p[0]; - const v = p[1]; - const x = modDivide(u, v, Fp); - const y = modDivide(u - one, u + one, Fp); - return [x, y]; -} - -/** -Converting Twisted Edwards point to Montgomery point -@param {String} p - point in Twisted Edwards form -*/ -function twistedEdwardsToMontgomery(p) { - if (p[0] === BigInt(0) && p[1] === BigInt(Fp - BigInt(1)) && p.length === 2) - return [BigInt(0), BigInt(0)]; // T -> M [0,-1] -> [0,0] - const x = p[0]; - const y = p[1]; - const u = modDivide(one + y, one - y, Fp); - const v = modDivide(one + y, (one - y) * x, Fp); - return [u, v]; -} - -/** -Performs El-Gamal cipherText -@param {Array(String)} strings - array containing the hex strings to be encrypted -@param {String} ephemeralKeys - random values mod Fq. They must be unique and changed each time this function is called -@param {String} publicKey - public key to encrypt with -*/ -function enc(ephemeralKeys, strings, publicKey) { - if (ephemeralKeys.length < strings.length) { - throw new Error( - 'The number of random secrets must be greater than or equal to the number of messages', - ); - } - // We can't directly encrypt hex strings. We can encrypt a curve point however, - // so we convert a string to a curve point using hash to curve - const messages = strings.map(e => hashToCurve(e)); - // We convert this to Twisted Edwards point because Elligator 2 is applied on top of TE and not Montgomery curves - const messagesTE = messages.map(e => { - return montgomeryToTwistedEdwards(e); - }); - // we get square roots calculated in hash to curve because it is quicker to prove squaring of two numbers than the square root value in zk circuits - const squareRootsElligator2 = strings.map(e => hashToCurveYSqrt(e)); - // now we use the public keys and random number to generate shared secrets - const sharedSecrets = ephemeralKeys.map(e => { - // eslint-disable-next-line valid-typeof - if (typeof e !== 'bigint') - throw new Error( - 'The random secret chosen for cipherText should be a BigInt, unlike the messages, which are hex strings', - ); - if (publicKey === undefined) throw new Error('Trying to encrypt with a undefined public key'); - return scalarMult(e, publicKey); - }); - // finally, we can encrypt the messages using the shared secrets - const c = ephemeralKeys.map(ephemeralKey => { - return scalarMult(ephemeralKey, GENERATOR); - }); - const encryptedMessages = messagesTE.map((message, i) => { - return add(message, sharedSecrets[i]); - }); - const cipherText = [...c, ...encryptedMessages]; - return { ephemeralKeys, cipherText, squareRootsElligator2 }; -} - -/** -Decrypt the above -*/ -function dec(cipherText, privateKey) { - const c = cipherText.slice(0, cipherText.length / 2); // this encrypts the sender's random secret, needed for shared-secret generation - const encryptedMessages = cipherText.slice(cipherText.length / 2, cipherText.length); - // recover the shared secrets - const sharedSecrets = c.map(sharedSecret => { - if (privateKey === undefined) - throw new Error('Trying to decrypt with an undefined private key'); - return scalarMult(privateKey, sharedSecret); - }); - // then decrypt - const messagePoints = encryptedMessages.map((encryptedMessage, i) => - add(encryptedMessage, negate(sharedSecrets[i])), - ); - const messagesMONT = messagePoints.map(messagePoint => twistedEdwardsToMontgomery(messagePoint)); - const messages = messagesMONT.map(messagePoint => curveToHash(messagePoint)); - return messages; -} - -/** A useful function that takes a curve point and throws away the x coordinate -retaining only the y coordinate and the odd/eveness of the x coordinate (plays the -part of a sign in mod arithmetic with a prime field). This loses no information -because we know the curve that relates x to y and the odd/eveness disabiguates the two -possible solutions. So it's a useful data compression. -TODO - probably simpler to use integer arithmetic rather than binary manipulations -*/ -function edwardsCompress(p) { - const px = p[0]; - const py = p[1]; - const xBits = px.toString(2).padStart(256, '0'); - const yBits = py.toString(2).padStart(256, '0'); - const sign = xBits[255] === '1' ? '1' : '0'; - const yBitsC = sign.concat(yBits.slice(1)); // add in the sign bit - const y = utils.ensure0x(BigInt('0b'.concat(yBitsC)).toString(16).padStart(64, '0')); // put yBits into hex - return y; -} - -function edwardsDecompress(y) { - const py = BigInt(y).toString(2).padStart(256, '0'); - const sign = py[0]; - const yfield = BigInt(`0b${py.slice(1)}`); // remove the sign encoding - if (yfield > Fp || yfield < 0) throw new Error(`y cordinate ${yfield} is not a field element`); - // 168700.x^2 + y^2 = 1 + 168696.x^2.y^2 - const y2 = mulMod([yfield, yfield], Fp); - const x2 = modDivide( - addMod([y2, BigInt(-1)], Fp), - addMod([mulMod([JUBJUBD, y2], Fp), -JUBJUBA], Fp), - Fp, - ); - if (x2 === 0n && sign === '0') return BABYJUBJUB.INFINITY; - let xfield = squareRootModPrime(x2, Fp); - const px = BigInt(xfield).toString(2).padStart(256, '0'); - if (px[255] !== sign) xfield = Fp - xfield; - const p = [xfield, yfield]; - if (!isOnCurve(p)) throw new Error('The computed point was not on the Babyjubjub curve'); - return p; -} - -export { dec, enc, scalarMult, edwardsCompress, edwardsDecompress }; diff --git a/wallet/src/nightfall-browser/utils/crypto/encryption/elligator2.js b/wallet/src/nightfall-browser/utils/crypto/encryption/elligator2.js deleted file mode 100644 index a436089ca..000000000 --- a/wallet/src/nightfall-browser/utils/crypto/encryption/elligator2.js +++ /dev/null @@ -1,107 +0,0 @@ -// ignore unused exports hashToCurve, hashToCurveYSqrt, curveToHash - -import { - squareRootModPrime, - addMod, - mulMod, - powerMod, -} from '../../../../common-files/utils/crypto/number-theory'; -import modDivide from './modular-division'; - -const { BABYJUBJUB, BN128_GROUP_ORDER, ELLIGATOR2 } = global.config; - -const one = BigInt(1); -const { MONTA, MONTB } = BABYJUBJUB; -const { U } = ELLIGATOR2; -const Fp = BN128_GROUP_ORDER; // the prime field used with the curve E(Fp) - -// χ : Fq → Fq by χ(a) = a^((q−1)/2) -function chi(a) { - return powerMod(a, (Fp - one) / BigInt(2), Fp); -} - -// // if value <= p-1//2, then positive -// function isPositive(value) { -// return value % Fp <= modDivide(BN128_GROUP_ORDER - BigInt(1), BigInt(2), Fp); -// } - -// if value > p-1//2, then negative -function isNegative(value) { - return value % Fp > modDivide(Fp - BigInt(1), BigInt(2), Fp); -} - -// if value == 0 or chi(value) == 1 -function isSquare(value) { - return value === BigInt(0) || chi(value) === BigInt(1); -} - -// r∈Fq :1+ur^2!=0, A^2ur^2!=B(1+ur^2)^2 -function checkR(r) { - return ( - (BigInt(1) + ((U * r * r) % Fp)) % Fp !== BigInt(0) && - (MONTA * MONTA * U * r * r) % Fp !== - (MONTB * (BigInt(1) + ((U * r * r) % Fp)) * (BigInt(1) + ((U * r * r) % Fp))) % Fp - ); -} - -// v = −A/(1+ur^2), -// ε = χ(v3 +Av2 +Bv), -// x = εv − (1 − ε)A/2, -// y = −ε sqrt(x3 +Ax2 +Bx) -// r has to be BigInt -export function hashToCurve(r) { - if (r === BigInt(0)) return [BigInt(0), BigInt(0)]; - if (checkR(r) !== true) throw new Error(`This value can't be hashed to curve using Elligator2`); - const v = modDivide(-MONTA, one + U * r * r, Fp); - const e = chi((v * v * v + MONTA * v * v + MONTB * v) % Fp); - const x = ((e * v) % Fp) - modDivide((one - e) * MONTA, BigInt(2), Fp); - let y2 = squareRootModPrime((x * x * x + MONTA * x * x + MONTB * x) % Fp, Fp); - // Ensure returned value is the principal root (i.e. sqrt(x) ∈ [0, (Fp -1) / 2] ) - if (y2 > (Fp - BigInt(1)) / BigInt(2)) y2 = Fp - y2; - const y = mulMod([-e, y2], Fp); - return [x, y]; -} - -// required for SNARK where we don't calculate the square root rather prove that the square of two -// square roots is the number for constraint efficiency -export function hashToCurveYSqrt(r) { - const v = modDivide(-MONTA, one + U * r * r, Fp); - const e = chi((v * v * v + MONTA * v * v + MONTB * v) % Fp); - const x = ((e * v) % Fp) - modDivide((one - e) * MONTA, BigInt(2), Fp); - let y2 = squareRootModPrime((x * x * x + MONTA * x * x + MONTB * x) % Fp, Fp); - // Ensure returned value is the principal root (i.e. sqrt(x) ∈ [0, (Fp -1) / 2] ) - if (y2 > (Fp - BigInt(1)) / BigInt(2)) y2 = Fp - y2; - return y2; -} - -// x=−A, -// if y=0 then x=0,and -// −ux(x+A) is a square in Fq. -function canCurveToHash(point) { - const x = point[0]; - const y = point[1]; - if (x === BigInt(0)) { - if (y !== BigInt(0)) { - return false; - } - } - return x !== -MONTA && isSquare(mulMod([mulMod([-U, x], Fp), addMod([x, MONTA], Fp)], Fp)); -} - -// r = sqrt(-x/(x+A)u), if y ∈ F2q -// r = sqrt(-(x+A)/ux), if y ∈/ F2q -export function curveToHash(point) { - if (point[0] === BigInt(0) && point[1] === BigInt(0) && point.length === 2) return BigInt(0); - if (!canCurveToHash(point)) throw new Error('cannot curve to hash'); - const x = point[0]; - const y = point[1]; - let r; - if (isNegative(y)) { - r = squareRootModPrime(modDivide(-(x + MONTA), U * x, Fp), Fp); - } else { - r = squareRootModPrime(modDivide(-x, U * (x + MONTA), Fp), Fp); - } - // Ensure returned value is the principal root (i.e. sqrt(x) ∈ [0, (Fp -1) / 2] ) - if (r > (Fp - BigInt(1)) / BigInt(2)) r = Fp - r; - return r; -} diff --git a/wallet/src/nightfall-browser/utils/crypto/encryption/modular-division.js b/wallet/src/nightfall-browser/utils/crypto/encryption/modular-division.js deleted file mode 100644 index 4ee2ce0ed..000000000 --- a/wallet/src/nightfall-browser/utils/crypto/encryption/modular-division.js +++ /dev/null @@ -1,42 +0,0 @@ -// ignore unused exports default - -// modular division - -const one = BigInt(1); -const zero = BigInt(0); - -// function for extended Euclidean Algorithm -// (used to find modular inverse. -function gcdExtended(a, b, _xy) { - const xy = _xy; - if (a === zero) { - xy[0] = zero; - xy[1] = one; - return b; - } - const xy1 = [zero, zero]; - const gcd = gcdExtended(b % a, a, xy1); - - // Update x and y using results of recursive call - xy[0] = xy1[1] - (b / a) * xy1[0]; - xy[1] = xy1[0]; // eslint-disable-line prefer-destructuring - - return gcd; -} - -// Function to find modulo inverse of b. -function modInverse(b, m) { - const xy = [zero, zero]; // used in extended GCD algorithm - const g = gcdExtended(b, m, xy); - if (g !== one) throw new Error('Numbers were not relatively prime'); - // m is added to handle negative x - return ((xy[0] % m) + m) % m; -} - -// Function to compute a/b mod m -export default function modDivide(a, b, m) { - const aa = ((a % m) + m) % m; // check the numbers are mod m and not negative - const bb = ((b % m) + m) % m; // do we really need this? - const inv = modInverse(bb, m); - return (inv * aa) % m; -} diff --git a/wallet/src/utils/CommitmentsBackup/commitmentsVerification.js b/wallet/src/utils/CommitmentsBackup/commitmentsVerification.js index 666947492..f9bacc1d6 100644 --- a/wallet/src/utils/CommitmentsBackup/commitmentsVerification.js +++ b/wallet/src/utils/CommitmentsBackup/commitmentsVerification.js @@ -1,7 +1,7 @@ /* ignore unused exports */ /** * - * @description this function should verify if the commitment's compressedPkd match with one of the derived keys. + * @description this function should verify if the commitment's compressedZkpPublicKey match with one of the derived keys. * If all match returns true else returns false. * @param {Object[]} indexedDBDerivedKeys the derived keys that you get from indexedDB keys objectStore. * @param {Object[]} commitmentsFromBackup the rows of commitments from the backup uploaded file. @@ -11,7 +11,7 @@ const isCommitmentsCPKDMatchDerivedKeys = (indexedDBDerivedKeys, commitmentsFrom return new Promise(resolve => { commitmentsFromBackup.forEach(commitment => { for (let i = 0; i < indexedDBDerivedKeys.length; i++) { - if (indexedDBDerivedKeys[i] === commitment.preimage.compressedPkd) { + if (indexedDBDerivedKeys[i] === commitment.preimage.compressedZkpPublicKey) { break; } if (i === indexedDBDerivedKeys.length - 1) { diff --git a/wallet/src/utils/lib/key-storage.ts b/wallet/src/utils/lib/key-storage.ts index a6c4d8b96..f941d4337 100644 --- a/wallet/src/utils/lib/key-storage.ts +++ b/wallet/src/utils/lib/key-storage.ts @@ -7,11 +7,9 @@ import { openDB } from 'idb'; type ZkpAccount = { - pkd: Array; - compressedPkd: string; - nsk: string; - ask: string; - ivk: string; + nullifierKey: string; + rootKey: string; + compressedZkpPublicKey: string; }; type CipherText = { @@ -63,7 +61,7 @@ export const encryptAndStore = async (acct: ZkpAccount): Promise => const db = await connectDB(); const key = await db.get(KEYS_COLLECTION, 'cryptokey'); const cipherText = await encrypt(acct, key); - return db.put(KEYS_COLLECTION, cipherText, acct.compressedPkd); + return db.put(KEYS_COLLECTION, cipherText, acct.compressedZkpPublicKey); }; const decrypt = async ( @@ -80,18 +78,18 @@ const decrypt = async ( return JSON.parse(decodeBuffer); // TODO error handling }; -export const retrieveAndDecrypt = async (compressedPkd: string): Promise => { +export const retrieveAndDecrypt = async (compressedZkpPublicKey: string): Promise => { const db = await connectDB(); const key = await db.get(KEYS_COLLECTION, 'cryptokey'); - const { cipherText, iv } = await db.get(KEYS_COLLECTION, compressedPkd); // TODO error handling + const { cipherText, iv } = await db.get(KEYS_COLLECTION, compressedZkpPublicKey); // TODO error handling return decrypt(Buffer.from(cipherText, 'base64'), key, iv); }; export const rotateKey = async (): Promise => { const db = await connectDB(); const keysCollection = await db.getAllKeys(KEYS_COLLECTION); - // Retrieve pkds - const compressedPkds = keysCollection.filter(k => k !== 'cryptokey'); + // Retrieve zkpPubKeys + const compressedZkpPublicKeys = keysCollection.filter(k => k !== 'cryptokey'); // Generate New Key const aesGenParams = { name: 'AES-GCM', length: 128 }; const newKey: CryptoKey = await crypto.subtle.generateKey(aesGenParams, false, [ @@ -100,13 +98,15 @@ export const rotateKey = async (): Promise => { ]); // Retrieve and Decrypt all accounts const decryptedZkpAccounts = await Promise.all( - compressedPkds.map(async (pkd: IDBValidKey) => retrieveAndDecrypt(pkd.toString())), + compressedZkpPublicKeys.map(async (recipientPublicKey: IDBValidKey) => + retrieveAndDecrypt(recipientPublicKey.toString()), + ), ); // Re-encrypt these accounts under the new key and store. await Promise.all( decryptedZkpAccounts.map(async dec => { const cipherText = await encrypt(dec, newKey); - return db.put(KEYS_COLLECTION, cipherText, dec.compressedPkd); + return db.put(KEYS_COLLECTION, cipherText, dec.compressedZkpPublicKey); }), ); // Store the new key. diff --git a/wallet/src/utils/lib/local-storage.js b/wallet/src/utils/lib/local-storage.js index c400f9784..f3a8136ba 100644 --- a/wallet/src/utils/lib/local-storage.js +++ b/wallet/src/utils/lib/local-storage.js @@ -31,13 +31,13 @@ function clear() { storage.clear(); } -function pkdArraySet(userKey, pkds) { +function ZkpPubKeyArraySet(userKey, zkpPubKeys) { init(); - storage.setItem(`${userKey}/pkds`, JSON.stringify(pkds)); + storage.setItem(`${userKey}/zkpPubKeys`, JSON.stringify(zkpPubKeys)); } -function pkdArrayGet(userKey) { - return JSON.parse(storage.getItem(`${userKey}/pkds`)); +function ZkpPubKeyArrayGet(userKey) { + return JSON.parse(storage.getItem(`${userKey}/zkpPubKeys`)); } async function setPricing(tokenIDs) { @@ -100,8 +100,8 @@ export { tokensSet, tokensGet, clear, - pkdArrayGet, - pkdArraySet, + ZkpPubKeyArrayGet, + ZkpPubKeyArraySet, setPricing, getPricing, shieldAddressGet, diff --git a/wallet/src/views/wallet/index.jsx b/wallet/src/views/wallet/index.jsx index 5e1b8f3a7..2ebd25e1d 100644 --- a/wallet/src/views/wallet/index.jsx +++ b/wallet/src/views/wallet/index.jsx @@ -485,8 +485,8 @@ export default function Wallet() { }, []); useEffect(async () => { - const pkdsDerived = Storage.pkdArrayGet(''); - if (state.compressedPkd === '' && !pkdsDerived) setModalShow(true); + const zkpPubKeysDerived = Storage.ZkpPubKeyArrayGet(''); + if (state.compressedZkpPublicKey === '' && !zkpPubKeysDerived) setModalShow(true); else setModalShow(false); }, []); @@ -498,12 +498,12 @@ export default function Wallet() { }, []); useInterval(async () => { - const l2BalanceObj = await getWalletBalance(state.compressedPkd); + const l2BalanceObj = await getWalletBalance(state.compressedZkpPublicKey); const updatedState = await Promise.all( tokens.map(async t => { const currencyValue = currencyValues[t.id]; - if (Object.keys(l2BalanceObj).includes(state.compressedPkd)) { - const token = l2BalanceObj[state.compressedPkd][t.address.toLowerCase()] ?? 0; + if (Object.keys(l2BalanceObj).includes(state.compressedZkpPublicKey)) { + const token = l2BalanceObj[state.compressedZkpPublicKey][t.address.toLowerCase()] ?? 0; return { ...t, l2Balance: token.toString(), diff --git a/wallet/tests/e2e/specs/e2e-spec.js b/wallet/tests/e2e/specs/e2e-spec.js index 5203a6d46..230d97f99 100644 --- a/wallet/tests/e2e/specs/e2e-spec.js +++ b/wallet/tests/e2e/specs/e2e-spec.js @@ -141,17 +141,17 @@ describe('End to End tests', () => { const transferValue = 4; /* - * dummy pkd of user who does not exist + * dummy recipientPublicKey of user who does not exist * Note: even though we are passing recipientPkd, but in code * it gets override by sender's pdk for now * hence two check */ - const recipientPkd = Cypress.env('RECIPIENT_PKD') || ' '; + const recipientZkpPublicKey = Cypress.env('RECIPIENT_PKD') || ' '; it(`transfer token of value ${transferValue}`, () => { cy.get('#TokenItem_tokenSendMATIC').click(); cy.get('#TokenItem_modalSend_tokenAmount').clear().type(transferValue); - cy.get('#TokenItem_modalSend_compressedPkd').clear().type(recipientPkd); + cy.get('#TokenItem_modalSend_compressedZkpPublicKey').clear().type(recipientZkpPublicKey); cy.get('button').contains('Continue').click(); cy.contains('L2 Bridge', { timeout: 10000 }).click(); cy.wait(10000); @@ -191,17 +191,17 @@ describe('End to End tests', () => { const returnValue = commitmentValues - transferValue; /* - * dummy pkd of user who does not exist + * dummy recipientPublicKey of user who does not exist * Note: even though we are passing recipientPkd, but in code * it gets override by sender's pdk for now * hence two check */ - const recipientPkd = Cypress.env('RECIPIENT_PKD') || ' '; + const recipientZkpPublicKey = Cypress.env('RECIPIENT_PKD') || ' '; it(`transfer token of value ${transferValue}`, () => { cy.get('#TokenItem_tokenSendMATIC').click(); cy.get('#TokenItem_modalSend_tokenAmount').clear().type(transferValue); - cy.get('#TokenItem_modalSend_compressedPkd').clear().type(recipientPkd); + cy.get('#TokenItem_modalSend_compressedZkpPublicKey').clear().type(recipientZkpPublicKey); cy.get('button').contains('Continue').click(); cy.wait(50000); cy.contains('L2 Bridge').click(); diff --git a/wallet/tests/unit/utils/CommitmentsBackup/commitmentsVerification.test.js b/wallet/tests/unit/utils/CommitmentsBackup/commitmentsVerification.test.js index 077bd2304..ea23200d6 100644 --- a/wallet/tests/unit/utils/CommitmentsBackup/commitmentsVerification.test.js +++ b/wallet/tests/unit/utils/CommitmentsBackup/commitmentsVerification.test.js @@ -4,9 +4,9 @@ const wrongCommitments = [ { table: 'commitments', rows: [ - { _id: '1', value: '1', preimage: { compressedPkd: '500' } }, - { _id: '2', value: '2', preimage: { compressedPkd: '200' } }, - { _id: '3', value: '3', preimage: { compressedPkd: '100' } }, + { _id: '1', value: '1', preimage: { compressedZkpPublicKey: '500' } }, + { _id: '2', value: '2', preimage: { compressedZkpPublicKey: '200' } }, + { _id: '3', value: '3', preimage: { compressedZkpPublicKey: '100' } }, ], }, ]; @@ -15,14 +15,14 @@ const mockObject = [ { table: 'commitments', rows: [ - { _id: '1', value: '1', preimage: { compressedPkd: '100' } }, - { _id: '2', value: '2', preimage: { compressedPkd: '200' } }, - { _id: '3', value: '3', preimage: { compressedPkd: '100' } }, + { _id: '1', value: '1', preimage: { compressedZkpPublicKey: '100' } }, + { _id: '2', value: '2', preimage: { compressedZkpPublicKey: '200' } }, + { _id: '3', value: '3', preimage: { compressedZkpPublicKey: '100' } }, ], }, ]; -describe('This suit test should insert some keys in a fake indexedDB and test the verification between commitmnets compressedPkds and these fake derived keys', () => { +describe('This suit test should insert some keys in a fake indexedDB and test the verification between commitmnets compressedZkpPublicKeys and these fake derived keys', () => { let objResult; beforeAll(async () => { objResult = ['100', '200', '300'];