diff --git a/common-files/classes/transaction.mjs b/common-files/classes/transaction.mjs index a5376e303..638940cab 100644 --- a/common-files/classes/transaction.mjs +++ b/common-files/classes/transaction.mjs @@ -14,6 +14,13 @@ const { generalise } = gen; const TOKEN_TYPES = { ERC20: 0, ERC721: 1, ERC1155: 2 }; const { TRANSACTION_TYPES } = constants; +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(); @@ -30,7 +37,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, @@ -57,7 +64,7 @@ class Transaction { // them undefined work?) constructor({ fee, - historicRootBlockNumberL2, + historicRootBlockNumberL2: _historicRoot, transactionType, tokenType, tokenId, @@ -69,11 +76,13 @@ 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; @@ -82,8 +91,11 @@ class Transaction { else nullifiers = _nullifiers; 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({ @@ -143,7 +155,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/config/default.js b/config/default.js index 8afeba74f..801dd1991 100644 --- a/config/default.js +++ b/config/default.js @@ -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, single_transfer: 1, double_transfer: 2, withdraw: 3 }, // 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 }, // 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) MAX_PUBLIC_VALUES: { ERCADDRESS: 2n ** 161n - 1n, COMMITMENT: 2n ** 249n - 1n, diff --git a/nightfall-client/src/classes/commitment.mjs b/nightfall-client/src/classes/commitment.mjs index b508f824b..176571b06 100644 --- a/nightfall-client/src/classes/commitment.mjs +++ b/nightfall-client/src/classes/commitment.mjs @@ -30,7 +30,10 @@ class Commitment { // the compressedPkd 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 = ZkpKeys.compressZkpPublicKey(this.preimage.zkpPublicKey); + 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)); diff --git a/nightfall-client/src/event-handlers/block-proposed.mjs b/nightfall-client/src/event-handlers/block-proposed.mjs index 3b0e1250a..388e916ab 100644 --- a/nightfall-client/src/event-handlers/block-proposed.mjs +++ b/nightfall-client/src/event-handlers/block-proposed.mjs @@ -76,12 +76,10 @@ async function blockProposedEventHandler(data, syncing) { } else if (transaction.transactionType === '0' && countOfNonZeroCommitments >= 1) { // case when deposit transaction created by user saveTxToDb = true; - } else if (transaction.transactionType === '3' && countOfNonZeroNullifiers >= 1) { - // case when withdraw transaction created by user - saveTxToDb = true; } - if (saveTxToDb) + if (saveTxToDb) { + logger.info('Saving Tx', transaction.transactionHash); await saveTransaction({ transactionHashL1, blockNumber: data.blockNumber, @@ -92,6 +90,7 @@ async function blockProposedEventHandler(data, syncing) { if (!syncing || !err.message.includes('replay existing transaction')) throw err; logger.warn('Attempted to replay existing transaction. This is expected while syncing'); }); + } return Promise.all([ saveTxToDb, diff --git a/nightfall-client/src/routes/transfer.mjs b/nightfall-client/src/routes/transfer.mjs index ed8a9530f..f8e83f0ed 100644 --- a/nightfall-client/src/routes/transfer.mjs +++ b/nightfall-client/src/routes/transfer.mjs @@ -10,10 +10,10 @@ const router = express.Router(); router.post('/', async (req, res, next) => { logger.debug(`transfer endpoint received POST ${JSON.stringify(req.body, null, 2)}`); try { - const { rawTransaction: txDataToSign, transaction, salts } = await transfer(req.body); + const { rawTransaction: txDataToSign, transaction } = await transfer(req.body); logger.debug('returning raw transaction'); logger.silly(` raw transaction is ${JSON.stringify(txDataToSign, null, 2)}`); - res.json({ txDataToSign, transaction, salts }); + res.json({ txDataToSign, transaction }); } catch (err) { logger.error(err); if (err.message.includes('No suitable commitments')) { diff --git a/nightfall-client/src/services/commitment-storage.mjs b/nightfall-client/src/services/commitment-storage.mjs index 73fac278b..cd36e45d7 100644 --- a/nightfall-client/src/services/commitment-storage.mjs +++ b/nightfall-client/src/services/commitment-storage.mjs @@ -82,7 +82,10 @@ export async function countNullifiers(nullifiers) { // function to get count of transaction hashes of withdraw type. Used to decide if we should store sibling path of transaction hash to be used later for finalising or instant withdrawal export async function countWithdrawTransactionHashes(transactionHashes) { const connection = await mongo.connection(MONGO_URL); - const query = { transactionHash: { $in: transactionHashes }, nullifierTransactionType: '3' }; + const query = { + transactionHash: { $in: transactionHashes }, + nullifierTransactionType: '2', + }; const db = connection.db(COMMITMENTS_DB); return db.collection(COMMITMENTS_COLLECTION).countDocuments(query); } @@ -90,7 +93,7 @@ export async function countWithdrawTransactionHashes(transactionHashes) { // function to get if the transaction hash belongs to a withdraw transaction export async function isTransactionHashWithdraw(transactionHash) { const connection = await mongo.connection(MONGO_URL); - const query = { transactionHash, nullifierTransactionType: '3' }; + const query = { transactionHash, nullifierTransactionType: '2' }; const db = connection.db(COMMITMENTS_DB); return db.collection(COMMITMENTS_COLLECTION).countDocuments(query); } @@ -511,7 +514,7 @@ export async function getWithdrawCommitments() { const db = connection.db(COMMITMENTS_DB); const query = { isNullified: true, - nullifierTransactionType: '3', + nullifierTransactionType: '2', isNullifiedOnChain: { $gte: 0 }, }; // Get associated nullifiers of commitments that have been spent on-chain and are used for withdrawals. @@ -617,12 +620,30 @@ async function findUsableCommitments(compressedZkpPublicKey, ercAddress, tokenId 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 a two-commitment transfer. The current strategy aims to prioritise smaller commitments while also - minimising the creation of low value commitments (dust) + /* 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). + + 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: @@ -636,6 +657,9 @@ async function findUsableCommitments(compressedZkpPublicKey, ercAddress, tokenId 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 @@ -643,6 +667,21 @@ async function findUsableCommitments(compressedZkpPublicKey, ercAddress, tokenId 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; + /** THIS WILL BE ENABLED LATED + 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, @@ -660,8 +699,8 @@ async function findUsableCommitments(compressedZkpPublicKey, ercAddress, tokenId } // 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) { @@ -672,7 +711,7 @@ async function findUsableCommitments(compressedZkpPublicKey, ercAddress, tokenId // 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; @@ -685,11 +724,10 @@ async function findUsableCommitments(compressedZkpPublicKey, ercAddress, tokenId logger.info( `Found commitments suitable for two-token transfer: ${JSON.stringify(commitmentsToUse)}`, ); - } else { - return null; + 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 diff --git a/nightfall-client/src/services/deposit.mjs b/nightfall-client/src/services/deposit.mjs index 91d1e6be7..35a6ca618 100644 --- a/nightfall-client/src/services/deposit.mjs +++ b/nightfall-client/src/services/deposit.mjs @@ -16,6 +16,7 @@ import constants from 'common-files/constants/index.mjs'; import { Commitment, Transaction } from '../classes/index.mjs'; import { storeCommitment } from './commitment-storage.mjs'; import { ZkpKeys } from './keys.mjs'; +import { computeWitness } from '../utils/compute-witness.mjs'; const { ZOKRATES_WORKER_HOST, PROVING_SCHEME, BACKEND, PROTOCOL, USE_STUBS, BN128_GROUP_ORDER } = config; @@ -33,14 +34,22 @@ async function deposit(items) { 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 witness = [ - ercAddress.field(BN128_GROUP_ORDER), - tokenId.limbs(32, 8), - value.field(BN128_GROUP_ORDER), - ...zkpPublicKey.all.field(BN128_GROUP_ORDER), - salt.field(BN128_GROUP_ORDER), - commitment.hash.field(BN128_GROUP_ORDER), - ].flat(Infinity); + 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 witness = computeWitness(publicData, roots, privateData); logger.debug(`witness input is ${witness.join(' ')}`); // call a zokrates worker to generate the proof let folderpath = 'deposit'; diff --git a/nightfall-client/src/services/transfer.mjs b/nightfall-client/src/services/transfer.mjs index 48853e5ff..cf146a04b 100644 --- a/nightfall-client/src/services/transfer.mjs +++ b/nightfall-client/src/services/transfer.mjs @@ -24,6 +24,7 @@ import { import getProposersUrl from './peers.mjs'; import { ZkpKeys } from './keys.mjs'; import { encrypt, genEphemeralKeys, packSecrets } from './kem-dem.mjs'; +import { computeWitness } from '../utils/compute-witness.mjs'; const { BN128_GROUP_ORDER, ZOKRATES_WORKER_HOST, PROVING_SCHEME, BACKEND, PROTOCOL, USE_STUBS } = config; @@ -76,14 +77,13 @@ async function transfer(transferParams) { const salts = await Promise.all(values.map(async () => randValueLT(BN128_GROUP_ORDER))); // Generate new commitments, already truncated to u32[7] - const newCommitments = recipientCompressedZkpPublicKeys.map( - (rcp, i) => + const newCommitments = values.map( + (value, i) => new Commitment({ ercAddress, tokenId, - value: values[i], + value, zkpPublicKey: recipientZkpPublicKeys[i], - // compressedZkpPublicKey: rcp, salt: salts[i].bigInt, }), ); @@ -100,7 +100,6 @@ async function transfer(transferParams) { // Compress the public key as it will be put on-chain const compressedEPub = edwardsCompress(ePublic); - const binaryEPub = generalise(compressedEPub).binary.padStart(256, '0'); // Commitment Tree Information const commitmentTreeInfo = await Promise.all(oldCommitments.map(c => getSiblingInfo(c))); @@ -111,72 +110,60 @@ async function transfer(transferParams) { const leafIndices = commitmentTreeInfo.map(l => l.leafIndex); const blockNumberL2s = commitmentTreeInfo.map(l => l.isOnChain); const roots = commitmentTreeInfo.map(l => l.root); - console.log( + 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) { + // time for a quick sanity check. We expect the number of old commitments and nullifiers to be equal. + if (nullifiers.length !== oldCommitments.length) { logger.error( - `number of old commitments: ${oldCommitments.length}, number of new commitments: ${newCommitments.length}, number of nullifiers: ${nullifiers.length}`, - ); - throw new Error( - 'Commitment or nullifier numbers are mismatched. There should be equal numbers of each', + `number of old commitments: ${oldCommitments.length}, number of nullifiers: ${nullifiers.length}`, ); + throw new Error('Number of nullifiers and old commitments are mismatched'); } // now we have everything we need to create a Witness and compute a proof - const witness = [ - oldCommitments.map(commitment => commitment.preimage.ercAddress.field(BN128_GROUP_ORDER)), - oldCommitments.map(commitment => [ - commitment.preimage.tokenId.limbs(32, 8), - commitment.preimage.value.limbs(8, 31), - commitment.preimage.salt.field(BN128_GROUP_ORDER), - commitment.hash.field(BN128_GROUP_ORDER), - rootKey.field(BN128_GROUP_ORDER), - ]), - newCommitments.map(commitment => [ - [ - commitment.preimage.zkpPublicKey[0].field(BN128_GROUP_ORDER), - commitment.preimage.zkpPublicKey[1].field(BN128_GROUP_ORDER), - ], - commitment.preimage.value.limbs(8, 31), - commitment.preimage.salt.field(BN128_GROUP_ORDER), - ]), - newCommitments.map(commitment => commitment.hash.field(BN128_GROUP_ORDER)), - nullifiers.map(nullifier => nullifier.hash.field(BN128_GROUP_ORDER)), - localSiblingPaths.map(siblingPath => siblingPath[0].field(BN128_GROUP_ORDER)), - localSiblingPaths.map(siblingPath => - siblingPath.slice(1).map(node => node.field(BN128_GROUP_ORDER)), - ), - leafIndices, - generalise(ePrivate).limbs(32, 8), - [binaryEPub[0], new GN(binaryEPub.slice(1), 'binary').field(BN128_GROUP_ORDER)], - compressedSecrets.map(c => generalise(c).field(BN128_GROUP_ORDER, false)), - ].flat(Infinity); + 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] + }), + ); + + 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: newCommitments.map(o => { + return { value: o.preimage.value, salt: o.preimage.salt }; + }), + recipientPublicKeys: newCommitments.map(o => o.preimage.zkpPublicKey), + ercAddress, + tokenId, + ephemeralKey: ePrivate, + }; + const witness = computeWitness(transaction, roots, privateData); logger.debug(`witness input is ${witness.join(' ')}`); // 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 folderpath; - let transactionType; - if (oldCommitments.length === 1) { - folderpath = 'single_transfer'; - transactionType = 1; - blockNumberL2s.push(0); // We need top pad block numbers if we do a single transfer - } else if (oldCommitments.length === 2) { - folderpath = 'double_transfer'; - transactionType = 2; - } else throw new Error('Unsupported number of commitments'); - if (USE_STUBS) folderpath = `${folderpath}_stub`; + let folderpath = 'transfer'; + if (USE_STUBS) folderpath = 'transfer_stub'; const res = await axios.post(`${PROTOCOL}${ZOKRATES_WORKER_HOST}/generate-proof`, { folderpath, - inputs: await witness, + inputs: witness, provingScheme: PROVING_SCHEME, backend: BACKEND, }); @@ -187,7 +174,7 @@ async function transfer(transferParams) { const optimisticTransferTransaction = new Transaction({ fee, historicRootBlockNumberL2: blockNumberL2s, - transactionType, + transactionType: 1, ercAddress: compressedSecrets[0], // this is the encrypted ercAddress tokenId: compressedSecrets[1], // this is the encrypted tokenID recipientAddress: compressedEPub, @@ -234,7 +221,6 @@ async function transfer(transferParams) { ); return { transaction: optimisticTransferTransaction, - salts: salts.map(salt => salt.hex(32)), }; } const rawTransaction = await shieldContractInstance.methods @@ -253,7 +239,6 @@ async function transfer(transferParams) { return { rawTransaction, transaction: optimisticTransferTransaction, - salts: salts.map(salt => salt.hex(32)), }; } catch (err) { await Promise.all(oldCommitments.map(commitment => clearPending(commitment))); diff --git a/nightfall-client/src/services/withdraw.mjs b/nightfall-client/src/services/withdraw.mjs index 60c75e464..6ec8a343a 100644 --- a/nightfall-client/src/services/withdraw.mjs +++ b/nightfall-client/src/services/withdraw.mjs @@ -11,69 +11,112 @@ import gen from 'general-number'; import { getContractInstance } from 'common-files/utils/contract.mjs'; import logger from 'common-files/utils/logger.mjs'; import constants from 'common-files/constants/index.mjs'; -import { Nullifier, Transaction } from '../classes/index.mjs'; +import { randValueLT } from 'common-files/utils/crypto/crypto-random.mjs'; +import { Commitment, Nullifier, Transaction } from '../classes/index.mjs'; import { findUsableCommitmentsMutex, markNullified, clearPending, getSiblingInfo, + storeCommitment, } from './commitment-storage.mjs'; import getProposersUrl from './peers.mjs'; import { ZkpKeys } from './keys.mjs'; +import { computeWitness } from '../utils/compute-witness.mjs'; const { BN128_GROUP_ORDER, ZOKRATES_WORKER_HOST, PROVING_SCHEME, BACKEND, PROTOCOL, USE_STUBS } = config; const { SHIELD_CONTRACT_NAME } = constants; -const { generalise } = gen; +const { generalise, GN } = gen; const NEXT_N_PROPOSERS = 3; +const MAX_WITHDRAW = 5192296858534827628530496329220096n; // 2n**112n async function withdraw(withdrawParams) { logger.info('Creating a withdraw transaction'); // let's extract the input items const { offchain = false, ...items } = withdrawParams; const { ercAddress, tokenId, value, recipientAddress, rootKey, fee } = generalise(items); - const { compressedZkpPublicKey, nullifierKey } = new ZkpKeys(rootKey); + const { compressedZkpPublicKey, nullifierKey, zkpPublicKey } = new ZkpKeys(rootKey); // 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( + 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 commitments ${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 // 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, nullifierKey); - // 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, ); - logger.silly(`SiblingPath was: ${JSON.stringify(siblingPath)}`); + const withdrawValue = value.bigInt > MAX_WITHDRAW ? MAX_WITHDRAW : value.bigInt; + const change = totalInputCommitmentValue - withdrawValue; + // and the Merkle path from the commitment to the root + // 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); + return generalise([l.root].concat(path.reverse())); + }); + const leafIndices = commitmentTreeInfo.map(l => l.leafIndex); + const blockNumberL2s = commitmentTreeInfo.map(l => l.isOnChain); + logger.silly(`SiblingPath was: ${JSON.stringify(localSiblingPaths)}`); - const { leafIndex, isOnChain } = commitmentTreeInfo; + 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, + }), + ); + } + const publicData = Transaction.buildSolidityStruct( + new Transaction({ + fee, + historicRootBlockNumberL2: blockNumberL2s, + commitments: newCommitment.length > 0 ? newCommitment : [{ hash: 0 }, { hash: 0 }], + transactionType: 2, + tokenType: items.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), + }; - // now we have everything we need to create a Witness and compute a proof - const witness = [ - oldCommitment.preimage.ercAddress.field(BN128_GROUP_ORDER), - oldCommitment.preimage.tokenId.limbs(32, 8), - oldCommitment.preimage.value.field(BN128_GROUP_ORDER), - oldCommitment.preimage.salt.field(BN128_GROUP_ORDER), - oldCommitment.hash.field(BN128_GROUP_ORDER), - rootKey.field(BN128_GROUP_ORDER), - nullifier.hash.field(BN128_GROUP_ORDER), - recipientAddress.field(BN128_GROUP_ORDER), - siblingPath[0].field(BN128_GROUP_ORDER), - siblingPath.slice(1).map(node => node.field(BN128_GROUP_ORDER)), // 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, - ].flat(Infinity); + const witness = computeWitness( + publicData, + localSiblingPaths.map(siblingPath => siblingPath[0]), + privateData, + ); logger.debug(`witness input is ${witness.join(' ')}`); // call a zokrates worker to generate the proof @@ -91,14 +134,15 @@ async function withdraw(withdrawParams) { const shieldContractInstance = await getContractInstance(SHIELD_CONTRACT_NAME); const optimisticWithdrawTransaction = new Transaction({ fee, - historicRootBlockNumberL2: [isOnChain, 0], - transactionType: 3, + historicRootBlockNumberL2: blockNumberL2s, + commitments: newCommitment.length > 0 ? newCommitment : [{ hash: 0 }, { hash: 0 }], + transactionType: 2, tokenType: items.tokenType, tokenId, value, ercAddress, recipientAddress, - nullifiers: [nullifier], + nullifiers, proof, }); try { @@ -117,8 +161,14 @@ async function withdraw(withdrawParams) { ); }), ); + // we store the change commitment + 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)), + ); const th = optimisticWithdrawTransaction.transactionHash; delete optimisticWithdrawTransaction.transactionHash; optimisticWithdrawTransaction.transactionHash = th; @@ -127,11 +177,17 @@ async function withdraw(withdrawParams) { const rawTransaction = await shieldContractInstance.methods .submitTransaction(Transaction.buildSolidityStruct(optimisticWithdrawTransaction)) .encodeABI(); + // we store the change commitment + 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)), + ); 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/nightfall-client/src/utils/compute-witness.mjs b/nightfall-client/src/utils/compute-witness.mjs new file mode 100644 index 000000000..a2336b287 --- /dev/null +++ b/nightfall-client/src/utils/compute-witness.mjs @@ -0,0 +1,117 @@ +import config from 'config'; +import gen from 'general-number'; + +const { generalise } = gen; +const { BN128_GROUP_ORDER } = config; + +const NULL_COMMITMENT = { + value: 0, + salt: 0, +}; +const padArray = (arr, padWith, n) => { + 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); +}; + +const computePublicInputs = (tx, roots) => { + const transaction = generalise(tx); + const rootsOldCommitments = padArray(generalise(roots), 0, 2); + let publicWitness = [ + transaction.value.field(BN128_GROUP_ORDER), + transaction.historicRootBlockNumberL2.map(h => h.field(BN128_GROUP_ORDER)), + transaction.transactionType.field(BN128_GROUP_ORDER), + transaction.tokenType.field(BN128_GROUP_ORDER), + transaction.tokenId.limbs(32, 8), + transaction.ercAddress.field(BN128_GROUP_ORDER), + transaction.recipientAddress.limbs(32, 8), + transaction.commitments.map(c => c.field(BN128_GROUP_ORDER)), + transaction.nullifiers.map(n => n.field(BN128_GROUP_ORDER)), + transaction.compressedSecrets.map(cs => cs.field(BN128_GROUP_ORDER)), + ]; + + if (Number(tx.transactionType) !== 0) { + publicWitness = [...publicWitness, rootsOldCommitments.map(r => r.field(BN128_GROUP_ORDER))]; + } + + return publicWitness.flat(Infinity); +}; + +const computePrivateInputsEncryption = privateData => { + const { ephemeralKey, ercAddress, tokenId } = generalise(privateData); + return [ + ephemeralKey.limbs(32, 8), + ercAddress.field(BN128_GROUP_ORDER), + tokenId.limbs(32, 8), + ].flat(Infinity); +}; + +const computePrivateInputsNullifiers = privateData => { + const { oldCommitmentPreimage, paths, orders, rootKey } = generalise(privateData); + const paddedOldCommitmentPreimage = padArray(oldCommitmentPreimage, NULL_COMMITMENT, 2); + const paddedPaths = padArray(paths, new Array(32).fill(0), 2); + const paddedOrders = padArray(orders, 0, 2); + const paddedRootKeys = padArray(rootKey, 0, 2); + + return [ + paddedOldCommitmentPreimage.map(commitment => commitment.value.limbs(8, 31)), + paddedOldCommitmentPreimage.map(commitment => commitment.salt.field(BN128_GROUP_ORDER)), + paddedRootKeys.map(r => r.field(BN128_GROUP_ORDER)), + paddedPaths.map(ps => ps.map(p => p.field(BN128_GROUP_ORDER))), + paddedOrders.map(m => m.field(BN128_GROUP_ORDER)), + ].flat(Infinity); +}; + +const computePrivateInputsCommitments = (privateData, padTo) => { + const { newCommitmentPreimage, recipientPublicKeys } = generalise(privateData); + const paddedNewCommitmentPreimage = padArray(newCommitmentPreimage, NULL_COMMITMENT, padTo); + const paddedRecipientPublicKeys = padArray(recipientPublicKeys, [0, 0], padTo); + return [ + paddedNewCommitmentPreimage.map(commitment => commitment.value.limbs(8, 31)), + paddedNewCommitmentPreimage.map(commitment => commitment.salt.field(BN128_GROUP_ORDER)), + paddedRecipientPublicKeys.map(rcp => [ + rcp[0].field(BN128_GROUP_ORDER), + rcp[1].field(BN128_GROUP_ORDER), + ]), + ].flat(Infinity); +}; + +const computePrivateInputsDeposit = privateData => { + const { salt, recipientPublicKeys } = generalise(privateData); + return [ + salt.field(BN128_GROUP_ORDER), + recipientPublicKeys.map(rcp => [ + rcp[0].field(BN128_GROUP_ORDER), + rcp[1].field(BN128_GROUP_ORDER), + ]), + ].flat(Infinity); +}; + +// eslint-disable-next-line import/prefer-default-export +export const computeWitness = (txObject, roots, privateData) => { + 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/nightfall-deployer/circuits/common/casts/u8_array_to_field.zok b/nightfall-deployer/circuits/common/casts/u8_array_to_field.zok index 5813774c4..b162f3765 100644 --- a/nightfall-deployer/circuits/common/casts/u8_array_to_field.zok +++ b/nightfall-deployer/circuits/common/casts/u8_array_to_field.zok @@ -1,6 +1,6 @@ from "EMBED" import u8_to_bits -def main(u8[N] i) -> (field): +def convert(u8[N] i) -> (field): field res = 0 for u32 k in 0..N do for u32 j in 0..8 do @@ -10,3 +10,10 @@ def main(u8[N] i) -> (field): endfor endfor return res + +def main(u8[M][N] input) -> (field[M]): + field[M] res = [0; M] + for u32 i in 0..M do + res[i] = convert(input[i]) + endfor + return res diff --git a/nightfall-deployer/circuits/common/generic_circuit/Stubs/commitments_stub.zok b/nightfall-deployer/circuits/common/generic_circuit/Stubs/commitments_stub.zok new file mode 100644 index 000000000..eecee571c --- /dev/null +++ b/nightfall-deployer/circuits/common/generic_circuit/Stubs/commitments_stub.zok @@ -0,0 +1,17 @@ +from "../../utils/structures.zok" import Point +from "../../utils/calculations.zok" import sum + +def main(\ + private field[NumCommitments] newCommitmentValues,\ + private field[NumCommitments] newCommitmentSalts,\ + private Point[NumCommitments] recipientPublicKey\ +)-> (bool): + + for u32 i in 0..NumCommitments do + field s = newCommitmentValues[i] * sum([\ + newCommitmentSalts[i],...recipientPublicKey[i]\ + ]) + assert(s == s) + endfor + + return true diff --git a/nightfall-deployer/circuits/common/generic_circuit/Stubs/encryption_stub.zok b/nightfall-deployer/circuits/common/generic_circuit/Stubs/encryption_stub.zok new file mode 100644 index 000000000..87d0103be --- /dev/null +++ b/nightfall-deployer/circuits/common/generic_circuit/Stubs/encryption_stub.zok @@ -0,0 +1,16 @@ +from "../../utils/calculations.zok" import sum +from "utils/pack/u32/pack256.zok" import main as u32_8_to_field + +def main(\ + private u32[8] ephemeralKey,\ + private field ercAddressTransfer,\ + private u32[8] idTransfer\ +)-> (bool): + + field s = ercAddressTransfer * sum([\ + u32_8_to_field(ephemeralKey),\ + u32_8_to_field(idTransfer)\ + ]) + assert(s == s) + + return true diff --git a/nightfall-deployer/circuits/common/generic_circuit/Stubs/nullifiers_stub.zok b/nightfall-deployer/circuits/common/generic_circuit/Stubs/nullifiers_stub.zok new file mode 100644 index 000000000..d506ec913 --- /dev/null +++ b/nightfall-deployer/circuits/common/generic_circuit/Stubs/nullifiers_stub.zok @@ -0,0 +1,20 @@ +from "../../utils/calculations.zok" import sum + +def main(\ + field[NumNullifiers] nullifierRoots,\ + private field[NumNullifiers] oldCommitmentValues,\ + private field[NumNullifiers] oldCommitmentSalts,\ + private field[NumNullifiers] rootKey,\ + private field[NumNullifiers][32] paths,\ + private field[NumNullifiers] orders\ +)-> (bool): + + for u32 i in 0..NumNullifiers do + field s = nullifierRoots[i] * sum([\ + rootKey[i], oldCommitmentValues[i],\ + oldCommitmentSalts[i],...paths[i], orders[i]\ + ]) + assert(s == s) + endfor + + return true \ No newline at end of file diff --git a/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_commitments.zok b/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_commitments.zok new file mode 100644 index 000000000..fcffee3db --- /dev/null +++ b/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_commitments.zok @@ -0,0 +1,43 @@ +from "hashes/poseidon/poseidon.zok" import main as poseidon +from "../../utils/structures.zok" import Point + +/* +* Verify that all commitments are correct +*/ +def main(\ + field packedErcAddress,\ + field idRemainder,\ + field[MaxCommitments] commitmentHashes,\ + private field[MaxCommitments] newCommitmentValues,\ + private field[MaxCommitments] newCommitmentSalts,\ + private Point[MaxCommitments] recipientPublicKey\ +) -> bool: + + //Check that all the commitments are valid. If NumCommitments equals zero this loop will be ignored + for u32 i in 0..MaxCommitments do + + /* + Calculate the commmitment hash from the newCommitment parameters + We can check if commitment is supposed to be null or non-null by the following + 1) Note that degenerate case for each circuit type (deposit, transfer, withdraw) also matches the possible uses involving erc721 + 2) Thus, we first perform checks based on this degenerate case (characterised by MinCommitments). + 3) We use (i + 1) as the loop is zero-indexed while MinCommitments is a count of minimum allowable commitments. + 4) Therefore we check (with a poseidon hash) any input when the incremented loop index matches this conditional. + 5) Finally, we check the "optional extra commitments" that are only allowable to erc20/1155 (characterised by value > 0) + */ + + field commitment = if (newCommitmentValues[i] != 0 || (i+1) == MinCommitments) then\ + poseidon([\ + packedErcAddress,\ + idRemainder,\ + newCommitmentValues[i],\ + ...recipientPublicKey[i],\ + newCommitmentSalts[i]\ + ]) else 0\ + fi + + //Check that the calculated commitment matches with the one contained in the transaction + assert(commitment == commitmentHashes[i]) + endfor + + return true diff --git a/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_encryption.zok b/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_encryption.zok new file mode 100644 index 000000000..6a4861a9f --- /dev/null +++ b/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_encryption.zok @@ -0,0 +1,39 @@ +from "ecc/edwardsCompress" import main as edwardsCompress +from "utils/pack/bool/nonStrictUnpack256.zok" import main as field_to_bool_256 +from "utils/casts/u32_8_to_bool_256.zok" import main as u32_8_to_bool_256 +from "../../casts/u32_array_to_field.zok" import main as u32_array_to_field +from "../../encryption/kem-dem.zok" import main as kemDem, EncryptedMsgs +from "../../utils/structures.zok" import Point + +/* +* Verify that the secrets are encrypted properly in the Transfer +*/ +def main(\ + field ercAddress,\ + u32[8] tokenId,\ + field[2] compressedSecrets,\ + field packedErcAddress,\ + field idRemainder,\ + private field newCommitmentValues,\ + private field newCommitmentSalts,\ + private Point recipientPublicKey,\ + u32[8] recipientAddress,\ + private u32[8] ephemeralKey\ +) -> bool: + + field[4] cipherText = [ercAddress,u32_array_to_field(tokenId),compressedSecrets[0],compressedSecrets[1]] + bool[256] bitEphemeralKey = u32_8_to_bool_256(ephemeralKey) + + field[4] plainTexts = [\ + packedErcAddress,\ + idRemainder,\ + newCommitmentValues,\ + newCommitmentSalts\ + ] + + EncryptedMsgs<4> enc = kemDem(bitEphemeralKey, [recipientPublicKey[0], recipientPublicKey[1]], plainTexts) + assert(cipherText == enc.cipherText) + + bool[256] compressedPubKeyOutput = edwardsCompress(enc.ephemeralPublicKey) + assert(compressedPubKeyOutput == u32_8_to_bool_256(recipientAddress)) + return true diff --git a/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_nullifiers.zok b/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_nullifiers.zok new file mode 100644 index 000000000..69dd2123d --- /dev/null +++ b/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_nullifiers.zok @@ -0,0 +1,69 @@ +from "ecc/babyjubjubParams" import BabyJubJubParams, main as curveParams +from "ecc/edwardsScalarMult" import main as scalarMult +from "utils/pack/bool/nonStrictUnpack256.zok" import main as field_to_bool_256 +from "hashes/poseidon/poseidon.zok" import main as poseidon +from "../../utils/structures.zok" import Point, PRIVATE_KEY_DOMAIN, NULLIFIER_KEY_DOMAIN +from "../../merkle-tree/path-check.zok" import main as pathCheck + +/* +* Verify that all the nullifiers are correct +*/ +def main(\ + field packedErcAddress,\ + field idRemainder,\ + field[MaxNullifiers] nullifierHashes,\ + field[MaxNullifiers] roots,\ + private field[MaxNullifiers] oldCommitmentValues,\ + private field[MaxNullifiers] oldCommitmentSalts,\ + private field[MaxNullifiers] rootKey,\ + private field[MaxNullifiers][32] paths,\ + private field[MaxNullifiers] orders\ +) -> (Point): + // Get Curve Params + BabyJubJubParams context = curveParams() + Point g = [context.Gu, context.Gv] + + //If the transaction contains change, the receiver of that change MUST be the same user that + //created the transaction. In this field[2] we will store the zkpPublicKey of the sender in case + //we need it to check the change. + Point firstInputZkpPublicKeys = [0,0] + + //Check that all the nullifiers are valid. If NumNullifiers equals zero this loop will be ignored + for u32 i in 0..MaxNullifiers do + // Calculation of zkpPrivateKey and nullifierKey from rootKey + field zkpPrivateKeys = poseidon([rootKey[i], PRIVATE_KEY_DOMAIN]) + field nullifierKeys = poseidon([rootKey[i], NULLIFIER_KEY_DOMAIN]) + + // Calculate zkpPublicKey + Point zkpPublicKeys = scalarMult(field_to_bool_256(zkpPrivateKeys), g, context) + + /* + Calculate the nullifier hash from the oldCommitment parameters + We can check if nullifier is supposed to be null or non-null by the following + 1) Note that degenerate case for each circuit type (deposit, transfer, withdraw) also matches the possible uses involving erc721 + 2) Thus, we first perform checks based on this degenerate case (characterised by MinNullifiers). + 3) We use (i + 1) as the loop is zero-indexed while MinNullifiers is a count of minimum allowable nullifiers. + 4) Therefore we check (with a poseidon hash/path check) any input when the incremented loop index matches this conditional. + 5) Finally, we check the "optional extra nullifiers" that are only allowable to erc20/1155 (characterised by value > 0) + */ + field calculatedOldCommitmentHash = poseidon([\ + packedErcAddress,\ + idRemainder,\ + oldCommitmentValues[i],\ + ...zkpPublicKeys,\ + oldCommitmentSalts[i]\ + ]) + field nullifier = if(oldCommitmentValues[i] != 0 || (i+1) == MinNullifiers) then poseidon([nullifierKeys, calculatedOldCommitmentHash]) else 0 fi + + //Check that the calculated nullifier matches with the one contained in the transaction + assert(nullifier == nullifierHashes[i]) + + //Check that the nullifier is contained in the tree + bool pathValidity = if(oldCommitmentValues[i] != 0 || (i+1) == MinNullifiers) then pathCheck([roots[i], ...paths[i]], orders[i], calculatedOldCommitmentHash) else true fi + assert(pathValidity) + + //Set the changeZkpPublicKeys if i = 0. Otherwise just set the same value + firstInputZkpPublicKeys = i == 0 ? zkpPublicKeys : firstInputZkpPublicKeys + endfor + + return firstInputZkpPublicKeys diff --git a/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_structure.zok b/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_structure.zok new file mode 100644 index 000000000..8977c2956 --- /dev/null +++ b/nightfall-deployer/circuits/common/generic_circuit/Verifiers/verify_structure.zok @@ -0,0 +1,63 @@ +from "../../casts/u32_array_to_field.zok" import main as u32_array_to_field +from "utils/casts/u32_to_field.zok" import main as u32_to_field +from "utils/pack/u32/nonStrictUnpack256.zok" import main as field_to_u32_8 + +/* +* Given a Public tx, check that the structure and all the public parameters are valid +*/ +def main(\ + field value,\ + field transactionType,\ + field tokenType,\ + u32[8] tokenId,\ + field ercAddress,\ + u32[8] recipientAddress,\ + field[2] commitments,\ + field[2] nullifiers,\ + field[2] historicRootBlockNumberL2,\ + field[2] compressedSecrets\ +) -> (bool): + + //Check that transaction type matches + assert(u32_to_field(TxType) == transactionType) + + //ErcAddress cannot be zero. In transfer will contain the encrypted version of the ercAddress belonging to the ciphertext + assert(ercAddress != 0) + + //Withdrawals will have a recipientAddress and also transfers, since we are using it as a way to send the public ephemeral key + assert((TxType == 0 && recipientAddress == field_to_u32_8(0))\ + || (TxType != 0 && recipientAddress != field_to_u32_8(0))) + + field id = u32_array_to_field(tokenId) + + //Transfers will have value equal to zero and id different than zero because it will contain the encrypted version + //of the id beloning to the CipherText + //For deposits and withdrawals, check that combination id and value matches the token type + //ERC20 -> Value > 0 and Id == 0 + //ERC721 -> Value == 0 + //ERC1155 -> Value > 0 + assert((TxType == 1 && value == 0)\ + || (TxType != 1 &&\ + (tokenType == 0 && value != 0 && id == 0)\ + || (tokenType == 1 && value == 0)\ + || (tokenType == 2 && value != 0))) + + //Check commitments + assert((TxType == 0 && commitments[0] != 0 && commitments[1] == 0) ||\ + (TxType == 1 && commitments[0] != 0 && commitments[0] != commitments[1]) ||\ + (TxType == 2 && commitments[1] == 0)) + + //Check nullifiers + assert(\ + (TxType == 0 && nullifiers[0] == 0 && nullifiers[1] == 0) || \ + ((TxType == 1 || TxType == 2) &&\ + (nullifiers[0] != 0 && nullifiers[0] != nullifiers[1])\ + )\ + ) + + //For transfers, compressedSecrets needs to have at least one element different than zero + //For deposits and withdrawals, compressedSecrets will be zero + assert((TxType == 1 && (compressedSecrets[0] != 0 || compressedSecrets[1] != 0))\ + || (TxType != 1 && compressedSecrets[0] == 0 && compressedSecrets[1] == 0)) + + return true diff --git a/nightfall-deployer/circuits/common/hashes/mimc/mimc-constants.zok b/nightfall-deployer/circuits/common/hashes/mimc/mimc-constants.zok deleted file mode 100644 index 799dfb3c6..000000000 --- a/nightfall-deployer/circuits/common/hashes/mimc/mimc-constants.zok +++ /dev/null @@ -1,94 +0,0 @@ -def main()->(field[91]): - return [ - 20888961410941983456478427210666206549300505294776164667214940546594746570981, - 15265126113435022738560151911929040668591755459209400716467504685752745317193, - 8334177627492981984476504167502758309043212251641796197711684499645635709656, - 1374324219480165500871639364801692115397519265181803854177629327624133579404, - 11442588683664344394633565859260176446561886575962616332903193988751292992472, - 2558901189096558760448896669327086721003508630712968559048179091037845349145, - 11189978595292752354820141775598510151189959177917284797737745690127318076389, - 3262966573163560839685415914157855077211340576201936620532175028036746741754, - 17029914891543225301403832095880481731551830725367286980611178737703889171730, - 4614037031668406927330683909387957156531244689520944789503628527855167665518, - 19647356996769918391113967168615123299113119185942498194367262335168397100658, - 5040699236106090655289931820723926657076483236860546282406111821875672148900, - 2632385916954580941368956176626336146806721642583847728103570779270161510514, - 17691411851977575435597871505860208507285462834710151833948561098560743654671, - 11482807709115676646560379017491661435505951727793345550942389701970904563183, - 8360838254132998143349158726141014535383109403565779450210746881879715734773, - 12663821244032248511491386323242575231591777785787269938928497649288048289525, - 3067001377342968891237590775929219083706800062321980129409398033259904188058, - 8536471869378957766675292398190944925664113548202769136103887479787957959589, - 19825444354178182240559170937204690272111734703605805530888940813160705385792, - 16703465144013840124940690347975638755097486902749048533167980887413919317592, - 13061236261277650370863439564453267964462486225679643020432589226741411380501, - 10864774797625152707517901967943775867717907803542223029967000416969007792571, - 10035653564014594269791753415727486340557376923045841607746250017541686319774, - 3446968588058668564420958894889124905706353937375068998436129414772610003289, - 4653317306466493184743870159523234588955994456998076243468148492375236846006, - 8486711143589723036499933521576871883500223198263343024003617825616410932026, - 250710584458582618659378487568129931785810765264752039738223488321597070280, - 2104159799604932521291371026105311735948154964200596636974609406977292675173, - 16313562605837709339799839901240652934758303521543693857533755376563489378839, - 6032365105133504724925793806318578936233045029919447519826248813478479197288, - 14025118133847866722315446277964222215118620050302054655768867040006542798474, - 7400123822125662712777833064081316757896757785777291653271747396958201309118, - 1744432620323851751204287974553233986555641872755053103823939564833813704825, - 8316378125659383262515151597439205374263247719876250938893842106722210729522, - 6739722627047123650704294650168547689199576889424317598327664349670094847386, - 21211457866117465531949733809706514799713333930924902519246949506964470524162, - 13718112532745211817410303291774369209520657938741992779396229864894885156527, - 5264534817993325015357427094323255342713527811596856940387954546330728068658, - 18884137497114307927425084003812022333609937761793387700010402412840002189451, - 5148596049900083984813839872929010525572543381981952060869301611018636120248, - 19799686398774806587970184652860783461860993790013219899147141137827718662674, - 19240878651604412704364448729659032944342952609050243268894572835672205984837, - 10546185249390392695582524554167530669949955276893453512788278945742408153192, - 5507959600969845538113649209272736011390582494851145043668969080335346810411, - 18177751737739153338153217698774510185696788019377850245260475034576050820091, - 19603444733183990109492724100282114612026332366576932662794133334264283907557, - 10548274686824425401349248282213580046351514091431715597441736281987273193140, - 1823201861560942974198127384034483127920205835821334101215923769688644479957, - 11867589662193422187545516240823411225342068709600734253659804646934346124945, - 18718569356736340558616379408444812528964066420519677106145092918482774343613, - 10530777752259630125564678480897857853807637120039176813174150229243735996839, - 20486583726592018813337145844457018474256372770211860618687961310422228379031, - 12690713110714036569415168795200156516217175005650145422920562694422306200486, - 17386427286863519095301372413760745749282643730629659997153085139065756667205, - 2216432659854733047132347621569505613620980842043977268828076165669557467682, - 6309765381643925252238633914530877025934201680691496500372265330505506717193, - 20806323192073945401862788605803131761175139076694468214027227878952047793390, - 4037040458505567977365391535756875199663510397600316887746139396052445718861, - 19948974083684238245321361840704327952464170097132407924861169241740046562673, - 845322671528508199439318170916419179535949348988022948153107378280175750024, - 16222384601744433420585982239113457177459602187868460608565289920306145389382, - 10232118865851112229330353999139005145127746617219324244541194256766741433339, - 6699067738555349409504843460654299019000594109597429103342076743347235369120, - 6220784880752427143725783746407285094967584864656399181815603544365010379208, - 6129250029437675212264306655559561251995722990149771051304736001195288083309, - 10773245783118750721454994239248013870822765715268323522295722350908043393604, - 4490242021765793917495398271905043433053432245571325177153467194570741607167, - 19596995117319480189066041930051006586888908165330319666010398892494684778526, - 837850695495734270707668553360118467905109360511302468085569220634750561083, - 11803922811376367215191737026157445294481406304781326649717082177394185903907, - 10201298324909697255105265958780781450978049256931478989759448189112393506592, - 13564695482314888817576351063608519127702411536552857463682060761575100923924, - 9262808208636973454201420823766139682381973240743541030659775288508921362724, - 173271062536305557219323722062711383294158572562695717740068656098441040230, - 18120430890549410286417591505529104700901943324772175772035648111937818237369, - 20484495168135072493552514219686101965206843697794133766912991150184337935627, - 19155651295705203459475805213866664350848604323501251939850063308319753686505, - 11971299749478202793661982361798418342615500543489781306376058267926437157297, - 18285310723116790056148596536349375622245669010373674803854111592441823052978, - 7069216248902547653615508023941692395371990416048967468982099270925308100727, - 6465151453746412132599596984628739550147379072443683076388208843341824127379, - 16143532858389170960690347742477978826830511669766530042104134302796355145785, - 19362583304414853660976404410208489566967618125972377176980367224623492419647, - 1702213613534733786921602839210290505213503664731919006932367875629005980493, - 10781825404476535814285389902565833897646945212027592373510689209734812292327, - 4212716923652881254737947578600828255798948993302968210248673545442808456151, - 7594017890037021425366623750593200398174488805473151513558919864633711506220, - 18979889247746272055963929241596362599320706910852082477600815822482192194401, - 13602139229813231349386885113156901793661719180900395818909719758150455500533, - 13952667105157556595308191233585255581771936717523666104281454907150877850313 -] diff --git a/nightfall-deployer/circuits/common/hashes/mimc/mimc-encryption.zok b/nightfall-deployer/circuits/common/hashes/mimc/mimc-encryption.zok deleted file mode 100644 index 367d5490e..000000000 --- a/nightfall-deployer/circuits/common/hashes/mimc/mimc-encryption.zok +++ /dev/null @@ -1,12 +0,0 @@ -// the mimc encryption function, used as the basis of hashing rounds -// encryption function - -import "./mimc-constants.zok" as constants - -def main(field x, field k)->(field): - field[91] c = constants() - for u32 i in 0..91 do - field t = x + c[i] + k - x = t**7 // t^7 because 7th power is bijective in this field - endfor - return x + k diff --git a/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-1.zok b/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-1.zok deleted file mode 100644 index 073ac39c3..000000000 --- a/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-1.zok +++ /dev/null @@ -1,8 +0,0 @@ -// MiMC hashing function for five input fields - -import "./mimc-encryption.zok" as mimcpe7 - -def main(field a)->(field): - field r = 0 - r = a + mimcpe7(a, r) - return r diff --git a/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-2.zok b/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-2.zok deleted file mode 100644 index b4da97670..000000000 --- a/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-2.zok +++ /dev/null @@ -1,10 +0,0 @@ -// MiMC hashing function for five input fields - -import "./mimc-encryption.zok" as mimcpe7 - -def main(field[2] a)->(field): - field r = 0 - for u32 i in 0..2 do - r = r + a[i] + mimcpe7(a[i], r) - endfor - return r diff --git a/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-5.zok b/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-5.zok deleted file mode 100644 index e5be18a71..000000000 --- a/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-5.zok +++ /dev/null @@ -1,10 +0,0 @@ -// MiMC hashing function for five input fields - -import "./mimc-encryption.zok" as mimcpe7 - -def main(field[5] a)->(field): - field r = 0 - for u32 i in 0..5 do - r = r + a[i] + mimcpe7(a[i], r) - endfor - return r diff --git a/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-N.zok b/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-N.zok deleted file mode 100644 index e95bf3bfa..000000000 --- a/nightfall-deployer/circuits/common/hashes/mimc/mimc-hash-N.zok +++ /dev/null @@ -1,10 +0,0 @@ -// MiMC hashing function for N input fields - -import "./mimc-encryption.zok" as mimcpe7 - -def main(field[N] a)->(field): - field r = 0 - for u32 i in 0..N do - r = r + a[i] + mimcpe7(a[i], r) - endfor - return r diff --git a/nightfall-deployer/circuits/common/utils/calculations.zok b/nightfall-deployer/circuits/common/utils/calculations.zok new file mode 100644 index 000000000..d15376034 --- /dev/null +++ b/nightfall-deployer/circuits/common/utils/calculations.zok @@ -0,0 +1,61 @@ + +from "ecc/edwardsCompress" import main as edwardsCompress +from "hashes/poseidon/poseidon.zok" import main as poseidon +from "utils/casts/u32_8_to_bool_256.zok" import main as u32_8_to_bool_256 +from "utils/pack/bool/nonStrictUnpack256.zok" import main as field_to_bool_256 +from "utils/pack/u32/pack256.zok" import main as u32_8_to_field +from "./structures.zok" import CommitmentPreimage, Point + +def sum(field[N] a) -> field: + field res = 0 + for u32 i in 0..N do + res = res + a[i] + endfor + return res + +def calculateCommitmentHashesN(\ + field packedErcAddress,\ + field idRemainder,\ + field[N] value,\ + Point[N] zkpPublicKeyRecipient,\ + field[N] salt\ + ) -> field[N]: + field[N] output = [0; N] + for u32 i in 0..N do + output[i] = poseidon([\ + packedErcAddress,\ + idRemainder,\ + value[i],\ + zkpPublicKeyRecipient[i][0],\ + zkpPublicKeyRecipient[i][1],\ + salt[i]\ + ]) + endfor + return output + +def calculateCommitmentHash(\ + field packedErcAddress,\ + field idRemainder,\ + field[1] value,\ + Point[1] zkpPublicKeyRecipient,\ + field[1] salt\ + ) -> field: + field[1] output = calculateCommitmentHashesN(packedErcAddress,idRemainder, value, zkpPublicKeyRecipient, salt) + return output[0] + + +def calculateNullifier(\ + field nullifierKey,\ + field commitmentHashes\ +) -> field: + return poseidon([nullifierKey,commitmentHashes]) + +def calculateNullifiersN(\ + field[N] nullifierKey,\ + field[N] commitmentHashes\ +) -> field[N]: + field[N] output = [0; N] + for u32 i in 0..N do + output[i] = calculateNullifier(nullifierKey[i], commitmentHashes[i]) + endfor + return output diff --git a/nightfall-deployer/circuits/common/utils/structures.zok b/nightfall-deployer/circuits/common/utils/structures.zok new file mode 100644 index 000000000..761641991 --- /dev/null +++ b/nightfall-deployer/circuits/common/utils/structures.zok @@ -0,0 +1,49 @@ +type Point = field[2] + +struct CommitmentPreimage { + u8[N][31] value + field[N] salt +} + + +const field PRIVATE_KEY_DOMAIN = 2708019456231621178814538244712057499818649907582893776052749473028258908910 +const field NULLIFIER_KEY_DOMAIN = 7805187439118198468809896822299973897593108379494079213870562208229492109015 + +// 2 ^ 160 +const field SHIFT = 1461501637330902918203684832716283019655932542976 + +struct CompressedPoint { + bool[N] parity + field[N] ordinate +} + +struct PublicTransaction { + field value + field[2] historicRootBlockNumberL2 + field transactionType + field tokenType + u32[8] tokenId + field ercAddress + u32[8] recipientAddress + field[2] commitments + field[2] nullifiers + field[2] compressedSecrets +} + +struct Nullifiers { + CommitmentPreimage oldCommitments + field[N] rootKey + field[N][32] paths + field[N] orders +} + +struct Commitments { + CommitmentPreimage newCommitments + Point[N] recipientPublicKey +} + +struct Transfer { + u32[8] ephemeralKey + field ercAddressTransfer + u32[8] idTransfer +} diff --git a/nightfall-deployer/circuits/deposit.zok b/nightfall-deployer/circuits/deposit.zok index 3057e57dd..c23ab0b6b 100644 --- a/nightfall-deployer/circuits/deposit.zok +++ b/nightfall-deployer/circuits/deposit.zok @@ -1,40 +1,38 @@ -from "hashes/poseidon/poseidon.zok" import main as poseidon -from "./common/casts/u32_array_to_field.zok" import main as u32_array_to_field - -// Inputs for main: -// - ercContractAddress (public) is the ERCx contract address -// - value (public) is the 256 bit value (fungible) or identifier (non-fungible) -// - publicKey (private) is the public key of the newCommitment derived by hashing the Secret Key Sk of the newCommitment. IT IS KEPT PRIVATE!! -// - salt (private) is the salt for the newCommitment -// - newCommitment (public) is the newCommitment -type Point = field[2] - -// 2 ^ 160 -const field SHIFT = 1461501637330902918203684832716283019655932542976 +from "./common/utils/structures.zok" import Point, PublicTransaction, Nullifiers, Commitments, Transfer, SHIFT +from "./common/utils/calculations.zok" import sum +from "./common/casts/u32_array_to_field.zok" import main as u32_array_to_field +from "./common/casts/u8_array_to_field.zok" import main as u8_array_to_field +from "./common/generic_circuit/Verifiers/verify_structure.zok" import main as verify_structure +from "./common/generic_circuit/Verifiers/verify_commitments.zok" import main as verify_commitments def main(\ - field ercContractAddress,\ - u32[8] id,\ - field value,\ - private Point compressedZkpPublicKey,\ - private field salt,\ - field newCommitment\ -)->(): + PublicTransaction tx,\ + private field salt,\ + private Point recipientPublicKey\ +)-> (): + + //Verify public transaction structure + assert(verify_structure::<0>(\ + tx.value,\ + tx.transactionType,\ + tx.tokenType,\ + tx.tokenId,\ + tx.ercAddress,\ + tx.recipientAddress,\ + tx.commitments,\ + tx.nullifiers,\ + tx.historicRootBlockNumberL2,\ + tx.compressedSecrets\ + )) - // pack the top four bytes of the token id into the ercAddress field (address only + // pack the top four bytes of the token id into the ercAddress field (address only // uses 160 bits and the Shield contract prevents creation of something with more than 160 bits) - field idTop4Bytes = u32_array_to_field([id[0]]) - field idRemainder = u32_array_to_field(id[1..8]) - field packedErcAddress = ercContractAddress + idTop4Bytes * SHIFT + field idRemainder = u32_array_to_field(tx.tokenId[1..8]) + field packedErcAddress = tx.ercAddress + u32_array_to_field([tx.tokenId[0]]) * SHIFT - field newCommitmentCheck = poseidon([ - packedErcAddress,\ - idRemainder,\ - value,\ - ...compressedZkpPublicKey,\ - salt\ - ]) - assert(newCommitmentCheck == newCommitment) + //Verify new Commmitments + assert(verify_commitments::<1,1>(packedErcAddress, idRemainder, [tx.commitments[0]],\ + [tx.value], [salt], [recipientPublicKey])) - return + return diff --git a/nightfall-deployer/circuits/deposit_stub.zok b/nightfall-deployer/circuits/deposit_stub.zok index f0910889c..aac979a4a 100644 --- a/nightfall-deployer/circuits/deposit_stub.zok +++ b/nightfall-deployer/circuits/deposit_stub.zok @@ -1,28 +1,15 @@ -// Inputs for main: -// - ercContractAddress (public) is the ERCx contract address -// - value (public) is the 256 bit value (fungible) or identifier (non-fungible) -// - compressedZkpPublicKey (private) is the public key of the newCommitment derived by hashing the Secret Key Sk of the newCommitment. IT IS KEPT PRIVATE!! -// - salt (private) is the salt for the newCommitment -// - newCommitment (public) is the newCommitment -type Point = field[2] - -struct TokenId { - field top4Bytes - field remainder -} +from "./common/utils/structures.zok" import PublicTransaction, Commitments, Nullifiers +from "./common/generic_circuit/Stubs/commitments_stub.zok" import main as commitment_stub +from "./common/generic_circuit/Stubs/nullifiers_stub.zok" import main as nullifier_stub +from "./common/casts/u8_array_to_field.zok" import main as u8_array_to_field def main(\ - field ercContractAddress,\ - u32[8] id,\ - field value,\ - private Point compressedZkpPublicKey,\ - private field salt,\ - field newCommitment\ -)->(): - - field u = compressedZkpPublicKey[0] * compressedZkpPublicKey[1] + ercContractAddress + value + newCommitment * salt - u32 v = id[0] * id[1] + id[2] + id[3] + id[4] + id[5] + id[6] * id[7] - assert(u==u) - assert (v==v) - return + PublicTransaction tx,\ + private field salt,\ + private field[2] recipientPublicKey\ +)-> (): + + assert(commitment_stub::<1>([tx.value], [salt], [recipientPublicKey])) + + return diff --git a/nightfall-deployer/circuits/double_transfer.zok b/nightfall-deployer/circuits/double_transfer.zok deleted file mode 100644 index 607a49dbf..000000000 --- a/nightfall-deployer/circuits/double_transfer.zok +++ /dev/null @@ -1,160 +0,0 @@ -from "ecc/babyjubjubParams" import BabyJubJubParams -from "ecc/babyjubjubParams" import main as curveParams -from "ecc/edwardsScalarMult" import main as scalarMult -from "ecc/edwardsCompress" import main as edwardsCompress -from "./common/encryption/kem-dem.zok" import main as kemDem, EncryptedMsgs - -from "utils/casts/u32_8_to_bool_256.zok" import main as u32_8_to_bool_256 -from "utils/pack/bool/nonStrictUnpack256.zok" import main as field_to_bool_256 -from "./common/casts/u8_array_to_field.zok" import main as u8_array_to_field -from "./common/casts/u32_array_to_field.zok" import main as u32_array_to_field - -from "hashes/poseidon/poseidon.zok" import main as poseidon -from "./common/merkle-tree/path-check.zok" import main as pathCheck - -type Point = field[2] - -// 2 ^ 160 -const field SHIFT = 1461501637330902918203684832716283019655932542976 - -struct OldCommitmentPreimage { - u32[8] id - u8[31] value - field salt - field hash - field rootKey -} - -struct NewCommitmentPreimage { - Point zkpPublicKeyRecipient - u8[31] value - field salt -} - -struct CompressedPoint { - bool parity - field ordinate -} - -def main(\ - private field[2] ercAddress,\ - private OldCommitmentPreimage[2] oldCommitment,\ - private NewCommitmentPreimage[2] newCommitment,\ - field[2] newCommitmentHash,\ - field[2] nullifier,\ - field[2] root,\ - private field[2][32] path,\ - private field[2] order,\ - private u32[8] ephemeralKey,\ - CompressedPoint compressedEphemeralPublicKey,\ - field[4] cipherText\ -)->(): - - BabyJubJubParams context = curveParams() - field[2] g = [context.Gu, context.Gv] - - // The domain numbers are derived thusly: - // keccak256('zkpPrivateKey') % BN128_GROUP_ORDER 2708019456231621178814538244712057499818649907582893776052749473028258908910 - // keccak256('nullifierKey') % BN128_GROUP_ORDER 7805187439118198468809896822299973897593108379494079213870562208229492109015 - - // Calculation of zkpPrivateKey and nullifierKey from rootKey - field[2] zkpPrivateKey = [\ - poseidon([oldCommitment[0].rootKey, 2708019456231621178814538244712057499818649907582893776052749473028258908910]),\ - poseidon([oldCommitment[1].rootKey, 2708019456231621178814538244712057499818649907582893776052749473028258908910])\ - ] - field[2] nullifierKey = [\ - poseidon([oldCommitment[0].rootKey, 7805187439118198468809896822299973897593108379494079213870562208229492109015]),\ - poseidon([oldCommitment[1].rootKey, 7805187439118198468809896822299973897593108379494079213870562208229492109015])\ - ] - bool[2][256] zkpPrivateKeyBool = [field_to_bool_256(zkpPrivateKey[0]), field_to_bool_256(zkpPrivateKey[1])] - field[2][2] zkpPublicKey = [scalarMult(zkpPrivateKeyBool[0], g, context), scalarMult(zkpPrivateKeyBool[1], g, context)] - - // constrain new commitment 1 to be 'change' - assert(newCommitment[1].zkpPublicKeyRecipient == zkpPublicKey[0]) - - //save values as fields (we know they can't overflow, so this is safe) - field[2] valueOld = [u8_array_to_field(oldCommitment[0].value), u8_array_to_field(oldCommitment[1].value)] - field[2] valueNew = [u8_array_to_field(newCommitment[0].value), u8_array_to_field(newCommitment[1].value)] - - // check the summation is correct - assert(valueOld[0] + valueOld[1] == valueNew[0] + valueNew[1]) - // check the two old commitments relate to the same ERC contract - assert(ercAddress[0] == ercAddress[1]) - // and are of the same type (they might not be for ERC1155) - assert(oldCommitment[0].id == oldCommitment[1].id) - // commitments can never be equal - assert(newCommitmentHash[0] != newCommitmentHash[1]) - // nullifiers can never be equal - assert(nullifier[0] != nullifier[1]) - - // check the nullifiers are valid - for u32 i in 0..2 do - field nullifierCheck = poseidon([\ - nullifierKey[i],\ - oldCommitment[i].hash\ - ]) - assert(nullifierCheck == nullifier[i]) - endfor - - // check the new commitment for recipient is valid - // firstly we need to pack the top four bytes of the TokenId into the address - // these are all static values so we only need do it once. - field idTop4Bytes = u32_array_to_field([oldCommitment[0].id[0]]) - field idRemainder = u32_array_to_field(oldCommitment[0].id[1..8]) - field packedErcAddress = ercAddress[0] + idTop4Bytes * SHIFT - field newCommitmentCheck = poseidon([\ - packedErcAddress,\ - idRemainder,\ - valueNew[0],\ - ...newCommitment[0].zkpPublicKeyRecipient,\ - newCommitment[0].salt\ - ]) - assert(newCommitmentCheck == newCommitmentHash[0]) - - // check the new commitment for sender is valid - newCommitmentCheck = poseidon([\ - packedErcAddress,\ - idRemainder,\ - valueNew[1],\ - ...zkpPublicKey[0],\ - newCommitment[1].salt\ - ]) - assert(newCommitmentCheck == newCommitmentHash[1]) - - // check the old commitments are valid - for u32 i in 0..2 do - field oldCommitmentCheck = poseidon([\ - packedErcAddress,\ - idRemainder,\ - valueOld[i],\ - ...zkpPublicKey[i],\ - oldCommitment[i].salt\ - ]) - assert(oldCommitmentCheck == oldCommitment[i].hash) - endfor - - // check that the old commitments are in the merkle tree - for u32 i in 0..2 do - field hash = oldCommitment[i].hash - assert(pathCheck([root[i], ...path[i]], order[i], hash)) - endfor - - // KEM-DEM Encryption - bool[256] bitEphemeralKey = u32_8_to_bool_256(ephemeralKey) - - field[4] plainTexts = [\ - packedErcAddress,\ - idRemainder,\ - valueNew[0],\ - newCommitment[0].salt\ - ] - EncryptedMsgs<4> enc = kemDem(bitEphemeralKey, newCommitment[0].zkpPublicKeyRecipient, plainTexts) - assert(cipherText == enc.cipherText) - - bool[256] compressedPubKeyOutput = edwardsCompress(enc.ephemeralPublicKey) - bool parity = compressedEphemeralPublicKey.parity - bool[256] ordinate = field_to_bool_256(compressedEphemeralPublicKey.ordinate) - bool[256] compressedCheck256 = [ parity, ...ordinate[1..256] ] - assert(compressedPubKeyOutput == compressedCheck256) - - return diff --git a/nightfall-deployer/circuits/double_transfer_stub.zok b/nightfall-deployer/circuits/double_transfer_stub.zok deleted file mode 100644 index 2564603e9..000000000 --- a/nightfall-deployer/circuits/double_transfer_stub.zok +++ /dev/null @@ -1,55 +0,0 @@ -type Point = field[2] - -struct OldCommitmentPreimage { - u32[8] id - u8[31] value - field salt - field hash - field rootKey -} - -struct NewCommitmentPreimage { - Point zkpPublicKeyRecipient - u8[31] value - field salt -} - -struct CompressedPoint { - bool parity - field ordinate -} - -def main(\ - private field[2] ercAddress,\ - private OldCommitmentPreimage[2] oldCommitment,\ - private NewCommitmentPreimage[2] newCommitment,\ - field[2] newCommitmentHash,\ - field[2] nullifier,\ - field[2] root,\ - private field[2][32] path,\ - private field[2] order,\ - private u32[8] ephemeralKey,\ - CompressedPoint compressedEphemeralPublicKey,\ - field[4] cipherText\ -)->(): - - field u = 0 - u32 v = 0 - u8 w = 0 - for u32 i in 0..2 do - u = ercAddress[i] + oldCommitment[i].salt + oldCommitment[i].hash + newCommitment[i].salt + newCommitment[i].zkpPublicKeyRecipient[0] + newCommitment[i].zkpPublicKeyRecipient[1] + nullifier[i] + root[i] + order[i] + oldCommitment[i].rootKey - for u32 j in 0..32 do - u = u * path[i][j] - endfor - v = oldCommitment[i].id[0] * oldCommitment[i].id[1] + oldCommitment[i].id[2] + oldCommitment[i].id[3] + oldCommitment[i].id[4] + oldCommitment[i].id[5] + oldCommitment[i].id[6] * oldCommitment[i].id[7] - for u32 j in 0..31 do - w = w + newCommitment[i].value[j] - endfor - endfor - - - assert(u == u) - assert(v == v) - assert(w == w) - - return diff --git a/nightfall-deployer/circuits/single_transfer.zok b/nightfall-deployer/circuits/single_transfer.zok deleted file mode 100644 index f797fcf66..000000000 --- a/nightfall-deployer/circuits/single_transfer.zok +++ /dev/null @@ -1,122 +0,0 @@ -from "ecc/babyjubjubParams" import BabyJubJubParams -from "ecc/babyjubjubParams" import main as curveParams -from "ecc/edwardsScalarMult" import main as scalarMult -from "ecc/edwardsCompress" import main as edwardsCompress -from "hashes/poseidon/poseidon.zok" import main as poseidon -from "./common/encryption/kem-dem" import main as kemDem, EncryptedMsgs - -from "utils/casts/u32_8_to_bool_256.zok" import main as u32_8_to_bool_256 -from "utils/pack/bool/nonStrictUnpack256.zok" import main as field_to_bool_256 - -from "./common/merkle-tree/path-check.zok" import main as pathCheck -from "./common/casts/u8_array_to_field.zok" import main as u8_array_to_field -from "./common/casts/u32_array_to_field.zok" import main as u32_array_to_field - -type Point = field[2] - -// 2 ^ 160 -const field SHIFT = 1461501637330902918203684832716283019655932542976 - -struct OldCommitmentPreimage { - u32[8] id - u8[31] value // needed to prevent overflow attacks in transfers - field salt - field hash - field rootKey -} - -struct NewCommitmentPreimage { - Point zkpPublicKeyRecipient - u8[31] value - field salt -} - -struct CompressedPoint { - bool parity - field ordinate -} - -def main(\ - private field ercAddress,\ - private OldCommitmentPreimage oldCommitment,\ - private NewCommitmentPreimage newCommitment,\ - field newCommitmentHash,\ - field nullifier,\ - field root,\ - private field[32] path,\ - private field order,\ - private u32[8] ephemeralKey,\ - CompressedPoint compressedEphemeralPublicKey,\ - field[4] cipherText\ -)->(): - - BabyJubJubParams context = curveParams() - field[2] g = [context.Gu, context.Gv] - - // Calculation of zkpPrivateKey and nullifierKey from rootKey - field zkpPrivateKey = poseidon([oldCommitment.rootKey, 2708019456231621178814538244712057499818649907582893776052749473028258908910]) - field nullifierKey = poseidon([oldCommitment.rootKey, 7805187439118198468809896822299973897593108379494079213870562208229492109015]) - bool[256] zkpPrivateKeyBool = field_to_bool_256(zkpPrivateKey) - field[2] zkpPublicKey = scalarMult(zkpPrivateKeyBool, g, context) - - // check the nullifier is valid - field nullifierCheck = poseidon([nullifierKey, oldCommitment.hash]) - assert (nullifierCheck == nullifier) - - // check the new commitment is valid - // we effectively throw away the value of the new commitment by insisting - // that it is equal to the old commitment value for a single-token transfer - // This is a little inefficient but makes the witness computation in node - // independent of how many commitments are being transformed. - assert(newCommitment.value == oldCommitment.value) - field value = u8_array_to_field(oldCommitment.value) - // pack the top four bytes of the token id into the ercAddress field (address only - // uses 160 bits and the Shield contract prevents creation of something with more than 160 bits) - field idTop4Bytes = u32_array_to_field([oldCommitment.id[0]]) - field idRemainder = u32_array_to_field(oldCommitment.id[1..8]) - field packedErcAddress = ercAddress + idTop4Bytes * SHIFT - - field newCommitmentCheck = poseidon([\ - packedErcAddress,\ - idRemainder,\ - value,\ - ...newCommitment.zkpPublicKeyRecipient,\ - newCommitment.salt\ - ]) - assert (newCommitmentCheck == newCommitmentHash) - - // check the old commitment is valid - field oldCommitmentCheck = poseidon([\ - packedErcAddress,\ - idRemainder,\ - value,\ - ...zkpPublicKey,\ - oldCommitment.salt\ - ]) - assert(oldCommitmentCheck == oldCommitment.hash) - - // check that the old commitment is in the merkle tree (path[0] should be the root) - field hash = oldCommitment.hash - bool x = pathCheck([root, ...path], order, hash) - assert(x) - - // KEM-DEM Encryption - bool[256] bitEphemeralKey = u32_8_to_bool_256(ephemeralKey) - - field[4] plainTexts = [\ - packedErcAddress,\ - idRemainder,\ - value,\ - newCommitment.salt\ - ] - - EncryptedMsgs<4> enc = kemDem(bitEphemeralKey, newCommitment.zkpPublicKeyRecipient, plainTexts) - assert(cipherText == enc.cipherText) - - bool[256] compressedPubKeyOutput = edwardsCompress(enc.ephemeralPublicKey) - bool parity = compressedEphemeralPublicKey.parity - bool[256] ordinate = field_to_bool_256(compressedEphemeralPublicKey.ordinate) - bool[256] compressedCheck256 = [ parity, ...ordinate[1..256] ] - assert(compressedPubKeyOutput == compressedCheck256) - - return diff --git a/nightfall-deployer/circuits/single_transfer_stub.zok b/nightfall-deployer/circuits/single_transfer_stub.zok deleted file mode 100644 index 802094695..000000000 --- a/nightfall-deployer/circuits/single_transfer_stub.zok +++ /dev/null @@ -1,53 +0,0 @@ -type Point = field[2] - -struct OldCommitmentPreimage { - u32[8] id - u8[31] value // needed to prevent overflow attacks in transfers - field salt - field hash - field rootKey -} - -struct NewCommitmentPreimage { - Point zkpPublicKeyRecipient - u8[31] value - field salt -} - -struct CompressedPoint { - bool parity - field ordinate -} - -def main(\ - private field ercAddress,\ - private OldCommitmentPreimage oldCommitment,\ - private NewCommitmentPreimage newCommitment,\ - field newCommitmentHash,\ - field nullifier,\ - field root,\ - private field[32] path,\ - private field order,\ - private u32[8] ephemeralKey,\ - CompressedPoint compressedEphemeralPublicKey,\ - field[4] cipherText\ -)->(): - - field u = ercAddress + oldCommitment.salt + oldCommitment.hash + newCommitment.salt + newCommitment.zkpPublicKeyRecipient[0] + newCommitment.zkpPublicKeyRecipient[1] + nullifier + root + order + oldCommitment.rootKey - - for u32 i in 0..32 do - u = u * path[i] - endfor - - u32 v = oldCommitment.id[0] * oldCommitment.id[1] + oldCommitment.id[2] + oldCommitment.id[3] + oldCommitment.id[4] + oldCommitment.id[5] + oldCommitment.id[6] * oldCommitment.id[7] - - u8 w = 0 - for u32 i in 0..31 do - w = w + newCommitment.value[i] - endfor - - assert(u == u) - assert(v == v) - assert(w == w) - - return diff --git a/nightfall-deployer/circuits/transfer.zok b/nightfall-deployer/circuits/transfer.zok new file mode 100644 index 000000000..5df8f04be --- /dev/null +++ b/nightfall-deployer/circuits/transfer.zok @@ -0,0 +1,62 @@ + +from "./common/utils/structures.zok" import Point, PublicTransaction, Nullifiers, Commitments, Transfer, SHIFT +from "./common/utils/calculations.zok" import sum +from "./common/casts/u32_array_to_field.zok" import main as u32_array_to_field +from "./common/casts/u8_array_to_field.zok" import main as u8_array_to_field +from "./common/generic_circuit/Verifiers/verify_structure.zok" import main as verify_structure +from "./common/generic_circuit/Verifiers/verify_encryption.zok" import main as verify_encryption +from "./common/generic_circuit/Verifiers/verify_nullifiers.zok" import main as verify_nullifiers +from "./common/generic_circuit/Verifiers/verify_commitments.zok" import main as verify_commitments + +def main(\ + PublicTransaction tx,\ + field[2] roots,\ + private Nullifiers<2> nullifiers,\ + private Commitments<2> commitments,\ + private Transfer transfer\ +)-> (): + + + //Verify public transaction structure + assert(verify_structure::<1>(\ + tx.value,\ + tx.transactionType,\ + tx.tokenType,\ + tx.tokenId,\ + tx.ercAddress,\ + tx.recipientAddress,\ + tx.commitments,\ + tx.nullifiers,\ + tx.historicRootBlockNumberL2,\ + tx.compressedSecrets\ + )) + + field[2] nullifiersValue = u8_array_to_field(nullifiers.oldCommitments.value) + field[2] commitmentsValue = u8_array_to_field(commitments.newCommitments.value) + + //Check that values match + assert(sum(nullifiersValue) == sum(commitmentsValue)) + + // pack the top four bytes of the token id into the ercAddress field (address only + // uses 160 bits and the Shield contract prevents creation of something with more than 160 bits) + field idRemainder = u32_array_to_field(transfer.idTransfer[1..8]) + field packedErcAddress = transfer.ercAddressTransfer + u32_array_to_field([transfer.idTransfer[0]]) * SHIFT + + //Verify nullifiers + Point firstInputZkpPublicKeys = verify_nullifiers::<1,2>(packedErcAddress, idRemainder,\ + tx.nullifiers, roots, nullifiersValue, nullifiers.oldCommitments.salt,\ + nullifiers.rootKey, nullifiers.paths, nullifiers.orders) + + //Verify new Commmitments + assert(verify_commitments::<1,2>(packedErcAddress, idRemainder, tx.commitments,\ + commitmentsValue, commitments.newCommitments.salt, commitments.recipientPublicKey)) + + //Verify Change + assert(commitmentsValue[1] == 0 || firstInputZkpPublicKeys == commitments.recipientPublicKey[1]) + + //Verify Kem Dem encryption + assert(verify_encryption(tx.ercAddress,tx.tokenId, tx.compressedSecrets,\ + packedErcAddress,idRemainder,commitmentsValue[0],commitments.newCommitments.salt[0],\ + commitments.recipientPublicKey[0],tx.recipientAddress, transfer.ephemeralKey)) + + return diff --git a/nightfall-deployer/circuits/transfer_stub.zok b/nightfall-deployer/circuits/transfer_stub.zok new file mode 100644 index 000000000..ea9aa480a --- /dev/null +++ b/nightfall-deployer/circuits/transfer_stub.zok @@ -0,0 +1,27 @@ +from "./common/utils/structures.zok" import Point, PublicTransaction, Nullifiers, Commitments, Transfer, SHIFT +from "./common/generic_circuit/Stubs/nullifiers_stub.zok" import main as nullifier_stub +from "./common/generic_circuit/Stubs/commitments_stub.zok" import main as commitment_stub +from "./common/generic_circuit/Stubs/encryption_stub.zok" import main as encryption_stub +from "./common/casts/u8_array_to_field.zok" import main as u8_array_to_field + + +def main(\ + PublicTransaction tx,\ + field[2] roots,\ + private Nullifiers<2> nullifiers,\ + private Commitments<2> commitments,\ + private Transfer transfer\ +)-> (): + + field[2] nullifiersValue = u8_array_to_field(nullifiers.oldCommitments.value) + field[2] commitmentsValue = u8_array_to_field(commitments.newCommitments.value) + + assert(nullifier_stub::<2>(\ + roots, nullifiersValue, nullifiers.oldCommitments.salt,\ + nullifiers.rootKey, nullifiers.paths, nullifiers.orders)) + + assert(commitment_stub::<2>(\ + commitmentsValue, commitments.newCommitments.salt, commitments.recipientPublicKey)) + + assert(encryption_stub(transfer.ephemeralKey, transfer.ercAddressTransfer, transfer.idTransfer)) + return diff --git a/nightfall-deployer/circuits/withdraw.zok b/nightfall-deployer/circuits/withdraw.zok index a2f842b35..cb7d8ea4f 100644 --- a/nightfall-deployer/circuits/withdraw.zok +++ b/nightfall-deployer/circuits/withdraw.zok @@ -1,70 +1,55 @@ -from "ecc/babyjubjubParams" import BabyJubJubParams -from "ecc/babyjubjubParams" import main as curveParams -from "ecc/edwardsCompress" import main as edwardsCompress -from "ecc/edwardsScalarMult" import main as scalarMult - -from "utils/pack/bool/nonStrictUnpack256.zok" import main as field_to_bool_256 -from "./common/casts/u8_array_to_field.zok" import main as u8_array_to_field +from "./common/utils/structures.zok" import Point, PublicTransaction, Nullifiers, Commitments, Transfer, SHIFT +from "./common/utils/calculations.zok" import sum from "./common/casts/u32_array_to_field.zok" import main as u32_array_to_field - -from "hashes/poseidon/poseidon.zok" import main as poseidon -from "./common/merkle-tree/path-check.zok" import main as pathCheck - -// 2 ^ 160 -const field SHIFT = 1461501637330902918203684832716283019655932542976 - -struct OldCommitmentPreimage { - field salt - field hash - field rootKey -} +from "./common/casts/u8_array_to_field.zok" import main as u8_array_to_field +from "./common/generic_circuit/Verifiers/verify_structure.zok" import main as verify_structure +from "./common/generic_circuit/Verifiers/verify_encryption.zok" import main as verify_encryption +from "./common/generic_circuit/Verifiers/verify_nullifiers.zok" import main as verify_nullifiers +from "./common/generic_circuit/Verifiers/verify_commitments.zok" import main as verify_commitments def main(\ - field ercAddress,\ - u32[8] id,\ - field value,\ - private OldCommitmentPreimage oldCommitment,\ - field nullifier,\ - field recipientAddress,\ - field root,\ - private field[32] path,\ - private field order\ -)->(): - - BabyJubJubParams context = curveParams() - field[2] g = [context.Gu, context.Gv] - - // Calculation of zkpPrivateKey and nullifierKey from rootKey - field zkpPrivateKey = poseidon([oldCommitment.rootKey, 2708019456231621178814538244712057499818649907582893776052749473028258908910]) - field nullifierKey = poseidon([oldCommitment.rootKey, 7805187439118198468809896822299973897593108379494079213870562208229492109015]) - bool[256] zkpPrivateKeyBool = field_to_bool_256(zkpPrivateKey) - field[2] zkpPublicKey = scalarMult(zkpPrivateKeyBool, g, context) + PublicTransaction tx,\ + field[2] roots,\ + private Nullifiers<2> nullifiers,\ + private Commitments<1> commitments\ +)-> (): + + //Verify public transaction structure + assert(verify_structure::<2>(\ + tx.value,\ + tx.transactionType,\ + tx.tokenType,\ + tx.tokenId,\ + tx.ercAddress,\ + tx.recipientAddress,\ + tx.commitments,\ + tx.nullifiers,\ + tx.historicRootBlockNumberL2,\ + tx.compressedSecrets\ + )) + + field[2] nullifiersValue = u8_array_to_field(nullifiers.oldCommitments.value) + field[1] commitmentsValue = u8_array_to_field(commitments.newCommitments.value) + + //Check that values match + assert(sum(nullifiersValue) == sum(commitmentsValue) + tx.value) + + // pack the top four bytes of the token id into the ercAddress field (address only + // uses 160 bits and the Shield contract prevents creation of something with more than 160 bits) + field idRemainder = u32_array_to_field(tx.tokenId[1..8]) + field packedErcAddress = tx.ercAddress + u32_array_to_field([tx.tokenId[0]]) * SHIFT - // check the nullifier is valid - field nullifierCheck = poseidon([\ - nullifierKey,\ - oldCommitment.hash\ - ]) - assert(nullifierCheck == nullifier) + //Verify nullifiers + Point firstInputZkpPublicKeys = verify_nullifiers::<1,2>(packedErcAddress, idRemainder,\ + tx.nullifiers, roots, nullifiersValue, nullifiers.oldCommitments.salt,\ + nullifiers.rootKey, nullifiers.paths, nullifiers.orders) - // pack the top four bytes of the token id into the ercAddress field (address only - // uses 160 bits and the Shield contract prevents creation of something with more than 160 bits) - field idTop4Bytes = u32_array_to_field([id[0]]) - field idRemainder = u32_array_to_field(id[1..8]) - field packedErcAddress = ercAddress + idTop4Bytes * SHIFT + //Verify new Commmitments + assert(verify_commitments::<0,1>(packedErcAddress, idRemainder, [tx.commitments[0]],\ + commitmentsValue, commitments.newCommitments.salt, commitments.recipientPublicKey)) - // check the old commitment is valid - field oldCommitmentCheck = poseidon([\ - packedErcAddress,\ - idRemainder,\ - value,\ - ...zkpPublicKey,\ - oldCommitment.salt\ - ]) - assert(oldCommitmentCheck == oldCommitment.hash) + //Verify Change + assert(commitmentsValue[0] == 0 || firstInputZkpPublicKeys == commitments.recipientPublicKey[0]) - // check that the old commitment is in the merkle tree - field hash = oldCommitment.hash - assert(pathCheck([root, ...path], order, hash)) + return - return diff --git a/nightfall-deployer/circuits/withdraw_stub.zok b/nightfall-deployer/circuits/withdraw_stub.zok index b6b1801d3..e1ee5db98 100644 --- a/nightfall-deployer/circuits/withdraw_stub.zok +++ b/nightfall-deployer/circuits/withdraw_stub.zok @@ -1,26 +1,24 @@ -struct OldCommitmentPreimage { - field salt - field hash - field rootKey -} +from "./common/utils/structures.zok" import Point, PublicTransaction, Nullifiers, Commitments, Transfer, SHIFT +from "./common/generic_circuit/Stubs/nullifiers_stub.zok" import main as nullifier_stub +from "./common/generic_circuit/Stubs/commitments_stub.zok" import main as commitment_stub +from "./common/casts/u8_array_to_field.zok" import main as u8_array_to_field + def main(\ - field ercAddress,\ - u32[8] id,\ - field value,\ - private OldCommitmentPreimage oldCommitment,\ - field nullifier,\ - field recipientAddress,\ - field root,\ - private field[32] path,\ - private field order\ -)->(): + PublicTransaction tx,\ + field[2] roots,\ + private Nullifiers<2> nullifiers,\ + private Commitments<2> commitments\ +)-> (): + + field[2] nullifiersValue = u8_array_to_field(nullifiers.oldCommitments.value) + field[2] commitmentsValue = u8_array_to_field(commitments.newCommitments.value) + + assert(nullifier_stub::<2>(\ + roots, nullifiersValue, nullifiers.oldCommitments.salt,\ + nullifiers.rootKey, nullifiers.paths, nullifiers.orders)) + + assert(commitment_stub::<2>(\ + commitmentsValue, commitments.newCommitments.salt, commitments.recipientPublicKey)) - field u = ercAddress + value + oldCommitment.salt + oldCommitment.hash + oldCommitment.rootKey + nullifier + recipientAddress + root + order - u32 v = id[0] + id[1] + id[2] + id[3] + id[4] + id[5] + id[6] * id[7] - for u32 i in 0..32 do - u = u * path[i] - endfor - assert(u==u) - assert (v==v) - return + return diff --git a/nightfall-deployer/contracts/Challenges.sol b/nightfall-deployer/contracts/Challenges.sol index 69f6e1302..7e4692706 100644 --- a/nightfall-deployer/contracts/Challenges.sol +++ b/nightfall-deployer/contracts/Challenges.sol @@ -46,13 +46,6 @@ contract Challenges is Stateful, Key_Registry, Config { priorBlockTransactions, blockL2.leafCount ); - // Now, we have an incorrect leafCount, but Timber relies on the leafCount - // emitted by the rollback event to revert its commitment database, so we - // need to correct the leafCount before we call challengeAccepted(...). - // We'll do that by counting forwards from the prior block. - blockL2.leafCount = - priorBlockL2.leafCount + - uint48(Utils.countCommitments(priorBlockTransactions)); challengeAccepted(blockL2); } @@ -121,120 +114,56 @@ contract Challenges is Stateful, Key_Registry, Config { } } - function challengeTransactionType( - Block memory blockL2, - Transaction[] memory transactions, - uint256 transactionIndex, - bytes32 salt - ) external onlyBootChallenger { - checkCommit(msg.data); - state.areBlockAndTransactionsReal(blockL2, transactions); - ChallengesUtil.libChallengeTransactionType(transactions[transactionIndex]); - // Delete the latest block of the two - challengeAccepted(blockL2); - } - - // signature for deposit: function challengeProofVerification( Block memory blockL2, Transaction[] calldata transactions, uint256 transactionIndex, + Block[2] calldata blockL2ContainingHistoricRoot, + Transaction[][2] memory transactionsOfblockL2ContainingHistoricRoot, uint256[8] memory uncompressedProof, bytes32 salt ) external onlyBootChallenger { checkCommit(msg.data); state.areBlockAndTransactionsReal(blockL2, transactions); - // first check the transaction and block do not overflow - ChallengesUtil.libCheckOverflows(blockL2, transactions[transactionIndex]); - // now we need to check that the proof is correct - ChallengesUtil.libCheckCompressedProof( - transactions[transactionIndex].proof, - uncompressedProof - ); - ChallengesUtil.libChallengeProofVerification( - transactions[transactionIndex], - [uint256(0), uint256(0)], - uncompressedProof, - vks[transactions[transactionIndex].transactionType] - ); - challengeAccepted(blockL2); - } - // signature for single transfer/withdraw: - function challengeProofVerification( - Block memory blockL2, - Transaction[] calldata transactions, - uint256 transactionIndex, - Block memory blockL2ContainingHistoricRoot, - Transaction[] memory transactionsOfblockL2ContainingHistoricRoot, - uint256[8] memory uncompressedProof, - bytes32 salt - ) external onlyBootChallenger { - checkCommit(msg.data); - state.areBlockAndTransactionsReal(blockL2, transactions); - state.areBlockAndTransactionsReal( - blockL2ContainingHistoricRoot, - transactionsOfblockL2ContainingHistoricRoot - ); - // check the historic root is in the block provided. - require( - transactions[transactionIndex].historicRootBlockNumberL2[0] == - blockL2ContainingHistoricRoot.blockNumberL2 - ); - // first check the transaction and block do not overflow - ChallengesUtil.libCheckOverflows(blockL2, transactions[transactionIndex]); - // now we need to check that the proof is correct - ChallengesUtil.libCheckCompressedProof( - transactions[transactionIndex].proof, - uncompressedProof - ); - ChallengesUtil.libChallengeProofVerification( - transactions[transactionIndex], - [uint256(blockL2ContainingHistoricRoot.root), uint256(0)], - uncompressedProof, - vks[transactions[transactionIndex].transactionType] - ); - challengeAccepted(blockL2); - } + uint256[2] memory roots; - // signature for double transfer: - function challengeProofVerification( - Block memory blockL2, - Transaction[] calldata transactions, - uint256 transactionIndex, - Block[2] calldata blockL2ContainingHistoricRoot, - Transaction[] memory transactionsOfblockL2ContainingHistoricRoot, - Transaction[] memory transactionsOfblockL2ContainingHistoricRoot2, - uint256[8] memory uncompressedProof, - bytes32 salt - ) external onlyBootChallenger { - checkCommit(msg.data); - state.areBlockAndTransactionsReal(blockL2, transactions); - state.areBlockAndTransactionsReal( - blockL2ContainingHistoricRoot[0], - transactionsOfblockL2ContainingHistoricRoot - ); - state.areBlockAndTransactionsReal( - blockL2ContainingHistoricRoot[1], - transactionsOfblockL2ContainingHistoricRoot2 - ); - // check the historic roots are in the blocks provided. - require( - transactions[transactionIndex].historicRootBlockNumberL2[0] == - blockL2ContainingHistoricRoot[0].blockNumberL2 && + if (uint256(transactions[transactionIndex].nullifiers[0]) != 0) { + state.areBlockAndTransactionsReal( + blockL2ContainingHistoricRoot[0], + transactionsOfblockL2ContainingHistoricRoot[0] + ); + require( + transactions[transactionIndex].historicRootBlockNumberL2[0] == + blockL2ContainingHistoricRoot[0].blockNumberL2, + 'Incorrect historic root block' + ); + roots[0] = uint256(blockL2ContainingHistoricRoot[0].root); + } else { + roots[0] = uint256(0); + } + + if (uint256(transactions[transactionIndex].nullifiers[1]) != 0) { + state.areBlockAndTransactionsReal( + blockL2ContainingHistoricRoot[1], + transactionsOfblockL2ContainingHistoricRoot[1] + ); + require( transactions[transactionIndex].historicRootBlockNumberL2[1] == - blockL2ContainingHistoricRoot[1].blockNumberL2, - 'Incorrect historic root block' - ); + blockL2ContainingHistoricRoot[1].blockNumberL2, + 'Incorrect historic root block' + ); + roots[1] = uint256(blockL2ContainingHistoricRoot[1].root); + } else { + roots[1] = uint256(0); + } + // first check the transaction and block do not overflow ChallengesUtil.libCheckOverflows(blockL2, transactions[transactionIndex]); // now we need to check that the proof is correct ChallengesUtil.libChallengeProofVerification( transactions[transactionIndex], - [ - uint256(blockL2ContainingHistoricRoot[0].root), - uint256(blockL2ContainingHistoricRoot[1].root) - ], + roots, uncompressedProof, vks[transactions[transactionIndex].transactionType] ); @@ -258,14 +187,15 @@ contract Challenges is Stateful, Key_Registry, Config { bytes32 salt ) external onlyBootChallenger { checkCommit(msg.data); + state.areBlockAndTransactionsReal(block1, txs1); + state.areBlockAndTransactionsReal(block2, txs2); + ChallengesUtil.libChallengeNullifier( txs1[transactionIndex1], nullifierIndex1, txs2[transactionIndex2], nullifierIndex2 ); - state.areBlockAndTransactionsReal(block1, txs1); - state.areBlockAndTransactionsReal(block2, txs2); // The blocks are different and we prune the later block of the two // as we have a block number, it's easy to see which is the latest. @@ -277,7 +207,7 @@ contract Challenges is Stateful, Key_Registry, Config { } /* - This checks if the historic root blockNumberL2 provided is greater than the numbe of blocks on-chain. + This checks if the historic root blockNumberL2 provided is greater than the number of blocks on-chain. If the root stored in the block is itself invalid, that is challengeable by challengeNewRootCorrect. the indices for the same nullifier in two **different** transactions contained in two blocks (note it should also be ok for the blocks to be the same) */ @@ -290,8 +220,8 @@ contract Challenges is Stateful, Key_Registry, Config { checkCommit(msg.data); state.areBlockAndTransactionsReal(blockL2, transactions); if ( - transactions[transactionIndex].transactionType == - Structures.TransactionTypes.DOUBLE_TRANSFER + uint256(transactions[transactionIndex].nullifiers[0]) != 0 && + uint256(transactions[transactionIndex].nullifiers[1]) != 0 ) { require( state.getNumberOfL2Blocks() < @@ -300,18 +230,16 @@ contract Challenges is Stateful, Key_Registry, Config { uint256(transactions[transactionIndex].historicRootBlockNumberL2[1]), 'Historic root exists' ); - } else if ( - transactions[transactionIndex].transactionType == Structures.TransactionTypes.DEPOSIT - ) { + } else if (uint256(transactions[transactionIndex].nullifiers[0]) == 0) { require( - uint256(transactions[transactionIndex].historicRootBlockNumberL2[0]) != 0 || + state.getNumberOfL2Blocks() < + uint256(transactions[transactionIndex].historicRootBlockNumberL2[0]) || uint256(transactions[transactionIndex].historicRootBlockNumberL2[1]) != 0, 'Historic root exists' ); } else { require( - state.getNumberOfL2Blocks() < - uint256(transactions[transactionIndex].historicRootBlockNumberL2[0]) || + uint256(transactions[transactionIndex].historicRootBlockNumberL2[0]) != 0 || uint256(transactions[transactionIndex].historicRootBlockNumberL2[1]) != 0, 'Historic root exists' ); @@ -331,7 +259,7 @@ contract Challenges is Stateful, Key_Registry, Config { // State.sol because Timber gets confused if its events come from two // different contracts (it uses the contract name as part of the db // connection - we need to change that). - state.emitRollback(badBlock.blockNumberL2, badBlock.leafCount); + state.emitRollback(badBlock.blockNumberL2); // we need to remove the block that has been successfully // challenged from the linked list of blocks and all of the subsequent // blocks @@ -363,7 +291,6 @@ contract Challenges is Stateful, Key_Registry, Config { // within the challenge function using this function: function checkCommit(bytes calldata messageData) private { bytes32 hash = keccak256(messageData); - // salt = 0; // not really required as salt is in msg.data but stops the unused variable compiler warning. Bit of a waste of gas though. require(committers[hash] == msg.sender, 'Commitment hash is invalid'); delete committers[hash]; } diff --git a/nightfall-deployer/contracts/ChallengesUtil.sol b/nightfall-deployer/contracts/ChallengesUtil.sol index a27b29041..e2708689f 100644 --- a/nightfall-deployer/contracts/ChallengesUtil.sol +++ b/nightfall-deployer/contracts/ChallengesUtil.sol @@ -32,145 +32,23 @@ library ChallengesUtil { Structures.Transaction[] memory transactions ) public pure { // next check the sibling path is valid and get the Frontier - bool valid; + bytes32 root; bytes32[33] memory _frontier; - (valid, _frontier) = MerkleTree_Stateless.checkPath( + (root, _frontier, ) = MerkleTree_Stateless.insertLeaves( Utils.filterCommitments(priorBlockTransactions), frontierPriorBlock, - priorBlockL2.leafCount, - priorBlockL2.root + priorBlockL2.leafCount ); - require(valid, 'The sibling path is invalid'); + require(root == priorBlockL2.root, 'The sibling path is invalid'); uint256 commitmentIndex = priorBlockL2.leafCount + Utils.filterCommitments(priorBlockTransactions).length; // At last, we can check if the root itself is correct! - (bytes32 root, , ) = - MerkleTree_Stateless.insertLeaves( - Utils.filterCommitments(transactions), - _frontier, - commitmentIndex - ); - require(root != blockL2.root, 'The root is actually fine'); - } - - // the transaction type is challenged to not be valid - function libChallengeTransactionType(Structures.Transaction memory transaction) public pure { - if (transaction.transactionType == Structures.TransactionTypes.DEPOSIT) - libChallengeTransactionTypeDeposit(transaction); - // TODO add these checks back after PR for out of gas - else if (transaction.transactionType == Structures.TransactionTypes.SINGLE_TRANSFER) - libChallengeTransactionTypeSingleTransfer(transaction); - else if (transaction.transactionType == Structures.TransactionTypes.DOUBLE_TRANSFER) - libChallengeTransactionTypeDoubleTransfer(transaction); // if(transaction.transactionType == TransactionTypes.WITHDRAW) - else libChallengeTransactionTypeWithdraw(transaction); - } - - // the transaction type deposit is challenged to not be valid - function libChallengeTransactionTypeDeposit(Structures.Transaction memory transaction) - public - pure - { - uint256 nZeroProof; - for (uint256 i = 0; i < transaction.proof.length; i++) { - if (transaction.proof[i] == 0) nZeroProof++; - } - uint256 nZeroCompressedSecrets; - for (uint256 i = 0; i < transaction.compressedSecrets.length; i++) { - if (transaction.compressedSecrets[i] == 0) nZeroCompressedSecrets++; - } - require( - (transaction.tokenId == ZERO && transaction.value == 0) || - transaction.ercAddress == ZERO || - transaction.recipientAddress != ZERO || - transaction.commitments[0] == ZERO || - transaction.commitments[1] != ZERO || - transaction.nullifiers[0] != ZERO || - transaction.nullifiers[1] != ZERO || - nZeroCompressedSecrets != 2 || - nZeroProof == 4 || // We assume that 3 out of the 4 proof elements can be a valid ZERO. Deals with exception cases - transaction.historicRootBlockNumberL2[0] != 0 || - transaction.historicRootBlockNumberL2[1] != 0, - 'This deposit transaction type is valid' - ); - } - - // the transaction type single transfer is challenged to not be valid - function libChallengeTransactionTypeSingleTransfer(Structures.Transaction memory transaction) - public - pure - { - uint256 nZeroCompressedSecrets; - for (uint256 i = 0; i < transaction.compressedSecrets.length; i++) { - if (transaction.compressedSecrets[i] == 0) nZeroCompressedSecrets++; - } - uint256 nZeroProof; - for (uint256 i = 0; i < transaction.proof.length; i++) { - if (transaction.proof[i] == 0) nZeroProof++; - } - require( - transaction.value != 0 || - transaction.commitments[0] == ZERO || - transaction.commitments[1] != ZERO || - transaction.nullifiers[0] == ZERO || - transaction.nullifiers[1] != ZERO || - nZeroCompressedSecrets == 2 || // We assume that 1 out of the 2 compressed secrets elements can be a valid ZERO. Deals with exception cases - nZeroProof == 4 || // We assume that 3 out of the 4 proof elements can be a valid ZERO. Deals with exception cases - transaction.historicRootBlockNumberL2[1] != 0, // If this is a single, the second historicBlockNumber needs to be zero - 'This single transfer transaction type is valid' - ); - } - - // the transaction type double transfer is challenged to not be valid - function libChallengeTransactionTypeDoubleTransfer(Structures.Transaction memory transaction) - public - pure - { - uint256 nZeroCompressedSecrets; - for (uint256 i = 0; i < transaction.compressedSecrets.length; i++) { - if (transaction.compressedSecrets[i] == 0) nZeroCompressedSecrets++; - } - uint256 nZeroProof; - for (uint256 i = 0; i < transaction.proof.length; i++) { - if (transaction.proof[i] == 0) nZeroProof++; - } - require( - transaction.value != 0 || - transaction.commitments[0] == ZERO || - transaction.commitments[1] == ZERO || - transaction.nullifiers[0] == ZERO || - transaction.nullifiers[1] == ZERO || - nZeroCompressedSecrets == 2 || // We assume that 1 out of the 2 compressed secrets elements can be a valid ZERO. Deals with exception cases - nZeroProof == 4, // We assume that 3 out of the 4 proof elements can be a valid ZERO. Deals with exception cases - 'This double transfer transaction type is valid' - ); - } - - // the transaction type withdraw is challenged to not be valid - function libChallengeTransactionTypeWithdraw(Structures.Transaction memory transaction) - public - pure - { - uint256 nZeroProof; - for (uint256 i = 0; i < transaction.proof.length; i++) { - if (transaction.proof[i] == 0) nZeroProof++; - } - uint256 nZeroCompressedSecrets; - for (uint256 i = 0; i < transaction.compressedSecrets.length; i++) { - if (transaction.compressedSecrets[i] == 0) nZeroCompressedSecrets++; - } - require( - (transaction.tokenId == ZERO && transaction.value == 0) || - transaction.ercAddress == ZERO || - transaction.recipientAddress == ZERO || - transaction.commitments[0] != ZERO || - transaction.commitments[1] != ZERO || - transaction.nullifiers[0] == ZERO || - transaction.nullifiers[1] != ZERO || - nZeroCompressedSecrets != 2 || - nZeroProof == 4 || // We assume that 3 out of the 4 proof elements can be a valid ZERO. Deals with exception cases - transaction.historicRootBlockNumberL2[1] != 0, // A withdraw has a similar constraint as a single transfer - 'This withdraw transaction type is valid' + (root, , ) = MerkleTree_Stateless.insertLeaves( + Utils.filterCommitments(transactions), + _frontier, + commitmentIndex ); + require(root != blockL2.root, 'The root is actually fine'); } function libChallengeProofVerification( @@ -207,13 +85,11 @@ library ChallengesUtil { ) public pure { require(uint256(transaction.ercAddress) <= MAX20, 'ERC address out of range'); require( - uint256(transaction.recipientAddress) <= MAX20, + (transaction.transactionType != Structures.TransactionTypes.TRANSFER && + uint256(transaction.recipientAddress) <= MAX20) || + (transaction.transactionType == Structures.TransactionTypes.TRANSFER), 'Recipient ERC address out of range' ); - require(uint256(transaction.commitments[0]) <= MAX31, 'Commitment 0 out of range'); - require(uint256(transaction.commitments[1]) <= MAX31, 'Commitment 1 out of range'); - require(uint256(transaction.nullifiers[0]) <= MAX31, 'Nullifier 0 out of range'); - require(uint256(transaction.nullifiers[1]) <= MAX31, 'Nullifier 1 out of range'); require(uint256(blockL2.root) < BN128_GROUP_ORDER, 'root out of range'); } diff --git a/nightfall-deployer/contracts/MerkleTree_Stateless.sol b/nightfall-deployer/contracts/MerkleTree_Stateless.sol index 0942d7321..f6e5f1dfb 100644 --- a/nightfall-deployer/contracts/MerkleTree_Stateless.sol +++ b/nightfall-deployer/contracts/MerkleTree_Stateless.sol @@ -190,129 +190,4 @@ library MerkleTree_Stateless { _leafCount += numberOfLeaves; // the incrememnting of leafCount costs us 20k for the first leaf, and 5k thereafter return (root, _frontier, _leafCount); //the root of the tree } - - function checkPath( - bytes32[33] memory siblingPath, - uint256 leafIndex, - bytes32 node - ) - public - pure - returns ( - /* bytes32 root */ - bool, - bytes32[33] memory - ) - { - bytes32[33] memory _frontier; - /* if (siblingPath[0] != root) return (false, _frontier); // check root of sibling path is actually the prior block root */ - // This is an incomplete check. Root parameter can be manipulated to pass the following check because this is not grounded to any data - uint256 nodeValue = uint256(node); - for (uint256 i = 32; i > 0; i--) { - _frontier[i] = bytes32(nodeValue); - if (leafIndex % 2 == 0) - nodeValue = Poseidon.poseidon(nodeValue, uint256(siblingPath[i])); - else nodeValue = Poseidon.poseidon(uint256(siblingPath[i]), nodeValue); - leafIndex >> 1; - } - _frontier[0] = node; - return (siblingPath[0] == node, _frontier); - } - - function checkPath( - bytes32[] memory leafValues, - bytes32[33] memory _frontier, - uint256 _leafCount, - bytes32 _root - ) public pure returns (bool, bytes32[33] memory) { - uint256 numberOfLeaves = leafValues.length; - - // check that space exists in the tree: - require(treeWidth > _leafCount, 'There is no space left in the tree.'); - if (numberOfLeaves > treeWidth - _leafCount) { - uint256 numberOfExcessLeaves = numberOfLeaves - (treeWidth - _leafCount); - // remove the excess leaves, because we only want to emit those we've added as an event: - for (uint256 xs = 0; xs < numberOfExcessLeaves; xs++) { - /* - CAUTION!!! This attempts to succinctly achieve leafValues.pop() on a **memory** dynamic array. Not thoroughly tested! - Credit: https://ethereum.stackexchange.com/a/51897/45916 - */ - - assembly { - mstore(leafValues, sub(mload(leafValues), 1)) - } - } - numberOfLeaves = treeWidth - _leafCount; - } - - uint256 slot; - uint256 nodeIndex; - uint256 prevNodeIndex; - uint256 nodeValue; - - uint256 output; // the output of the hash - - // consider each new leaf in turn, from left to right: - for (uint256 leafIndex = _leafCount; leafIndex < _leafCount + numberOfLeaves; leafIndex++) { - nodeValue = uint256(leafValues[leafIndex - _leafCount]); - nodeIndex = leafIndex + treeWidth - 1; // convert the leafIndex to a nodeIndex - - slot = getFrontierSlot(leafIndex); // determine at which level we will next need to store a nodeValue - - if (slot == 0) { - _frontier[slot] = bytes32(nodeValue); // update Frontier - continue; - } - - // hash up to the level whose nodeValue we'll store in the frontier slot: - for (uint256 level = 1; level <= slot; level++) { - if (nodeIndex % 2 == 0) { - // even nodeIndex - output = Poseidon.poseidon(uint256(_frontier[level - 1]), nodeValue); // poseidon hash of concatenation of each node - - nodeValue = output; // the parentValue, but will become the nodeValue of the next level - prevNodeIndex = nodeIndex; - nodeIndex = (nodeIndex - 1) / 2; // move one row up the tree - // emit Output(input, output, prevNodeIndex, nodeIndex); // for debugging only - } else { - // odd nodeIndex - output = Poseidon.poseidon(nodeValue, 0); // poseidon hash of concatenation of each node - - nodeValue = output; // the parentValue, but will become the nodeValue of the next level - prevNodeIndex = nodeIndex; - nodeIndex = nodeIndex / 2; // the parentIndex, but will become the nodeIndex of the next level - // emit Output(input, output, prevNodeIndex, nodeIndex); // for debugging only - } - } - _frontier[slot] = bytes32(nodeValue); // update frontier - } - - // So far we've added all leaves, and hashed up to a particular level of the tree. We now need to continue hashing from that level until the root: - for (uint256 level = slot + 1; level <= treeHeight; level++) { - if (nodeIndex % 2 == 0) { - // even nodeIndex - output = Poseidon.poseidon(uint256(_frontier[level - 1]), nodeValue); // poseidon hash of concatenation of each node - - nodeValue = output; // the parentValue, but will become the nodeValue of the next level - prevNodeIndex = nodeIndex; - nodeIndex = (nodeIndex - 1) / 2; // the parentIndex, but will become the nodeIndex of the next level - // emit Output(input, output, prevNodeIndex, nodeIndex); // for debugging only - } else { - // odd nodeIndex - output = Poseidon.poseidon(nodeValue, 0); // poseidon hash of concatenation of each node - - nodeValue = output; // the parentValue, but will become the nodeValue of the next level - prevNodeIndex = nodeIndex; - nodeIndex = nodeIndex / 2; // the parentIndex, but will become the nodeIndex of the next level - // emit Output(input, output, prevNodeIndex, nodeIndex); // for debugging only - } - } - - /* root = nodeValue; */ - - //emit NewLeaves(_leafCount, leafValues, root); // this event is what the merkle-tree microservice's filter will listen for. - - /* return (root, _frontier, _leafCount); //the root of the tree */ - return (_root == bytes32(nodeValue), _frontier); - } } diff --git a/nightfall-deployer/contracts/Shield.sol b/nightfall-deployer/contracts/Shield.sol index d9f6cdcb7..cf883c81d 100644 --- a/nightfall-deployer/contracts/Shield.sol +++ b/nightfall-deployer/contracts/Shield.sol @@ -274,7 +274,10 @@ contract Shield is Stateful, Config, Key_Registry, ReentrancyGuardUpgradeable, P function payIn(Transaction memory t) internal { // check the address fits in 160 bits. This is so we can't overflow the circuit uint256 addrNum = uint256(t.ercAddress); - require (addrNum < 0x010000000000000000000000000000000000000000, 'The given address is more than 160 bits'); + require( + addrNum < 0x010000000000000000000000000000000000000000, + 'The given address is more than 160 bits' + ); address addr = address(uint160(addrNum)); if (t.tokenType == TokenType.ERC20) { diff --git a/nightfall-deployer/contracts/State.sol b/nightfall-deployer/contracts/State.sol index d63780a92..b82304f47 100644 --- a/nightfall-deployer/contracts/State.sol +++ b/nightfall-deployer/contracts/State.sol @@ -27,10 +27,10 @@ contract State is Initializable, ReentrancyGuardUpgradeable, Pausable, Config { address public challengesAddress; address public shieldAddress; - function initialize() public override(Pausable, Config){ - Pausable.initialize(); - Config.initialize(); - ReentrancyGuardUpgradeable.__ReentrancyGuard_init(); + function initialize() public override(Pausable, Config) { + Pausable.initialize(); + Config.initialize(); + ReentrancyGuardUpgradeable.__ReentrancyGuard_init(); } function initialize( @@ -42,7 +42,7 @@ contract State is Initializable, ReentrancyGuardUpgradeable, Pausable, Config { challengesAddress = _challengesAddress; shieldAddress = _shieldAddress; initialize(); - } + } modifier onlyRegistered { require( @@ -160,15 +160,8 @@ contract State is Initializable, ReentrancyGuardUpgradeable, Pausable, Config { // it's uinque, although technically not needed (Optimist consumes the // block number and Timber the leaf count). It's helpful when testing to make // sure we have the correct event. - function emitRollback(uint256 blockNumberL2ToRollbackTo, uint256 leafCountToRollbackTo) - public - onlyRegistered - { - emit Rollback( - blockHashes[blockNumberL2ToRollbackTo].blockHash, - blockNumberL2ToRollbackTo, - leafCountToRollbackTo - ); + function emitRollback(uint256 blockNumberL2ToRollbackTo) public onlyRegistered { + emit Rollback(blockNumberL2ToRollbackTo); } function setProposer(address addr, LinkedAddress memory proposer) public onlyRegistered { diff --git a/nightfall-deployer/contracts/Structures.sol b/nightfall-deployer/contracts/Structures.sol index 8f8d6167c..4a511cf16 100644 --- a/nightfall-deployer/contracts/Structures.sol +++ b/nightfall-deployer/contracts/Structures.sol @@ -6,11 +6,11 @@ Basic data structures for an optimistic rollup pragma solidity ^0.8.0; contract Structures { - enum TransactionTypes {DEPOSIT, SINGLE_TRANSFER, DOUBLE_TRANSFER, WITHDRAW} + enum TransactionTypes {DEPOSIT, TRANSFER, WITHDRAW} enum TokenType {ERC20, ERC721, ERC1155} - event Rollback(bytes32 indexed blockHash, uint256 blockNumberL2, uint256 leafCount); + event Rollback(uint256 blockNumberL2); event BlockProposed(); diff --git a/nightfall-deployer/contracts/Utils.sol b/nightfall-deployer/contracts/Utils.sol index c0195fc43..55db2a491 100644 --- a/nightfall-deployer/contracts/Utils.sol +++ b/nightfall-deployer/contracts/Utils.sol @@ -25,7 +25,7 @@ library Utils { bytes32[] memory transactionHashes = new bytes32[](ts.length); for (uint256 i = 0; i < ts.length; i++) { - transactionHashes[i ] = hashTransaction(ts[i]); + transactionHashes[i] = hashTransaction(ts[i]); } transactionHashesRoot = calculateMerkleRoot(transactionHashes); return transactionHashesRoot; @@ -104,96 +104,52 @@ library Utils { pure returns (uint256[] memory inputs) { - // uint256[] memory inputs = new uint256[](countPublicInputs(ts)); - if (ts.transactionType == Structures.TransactionTypes.DEPOSIT) { - inputs = getDepositInputs(ts); - } else if (ts.transactionType == Structures.TransactionTypes.SINGLE_TRANSFER) { - inputs = getSingleTransferInputs(ts, roots); - } else if (ts.transactionType == Structures.TransactionTypes.DOUBLE_TRANSFER) { - inputs = getDoubleTransferInputs(ts, roots); - } else { - inputs = getWithdrawInputs(ts, roots); + inputs[0] = uint256(ts.value); + inputs[1] = uint256(ts.historicRootBlockNumberL2[0]); + inputs[2] = uint256(ts.historicRootBlockNumberL2[1]); + inputs[3] = uint256(ts.transactionType); + inputs[4] = uint256(ts.tokenType); + inputs[5] = uint32(uint256(ts.tokenId) >> 224); + inputs[6] = uint32(uint256(ts.tokenId) >> 192); + inputs[7] = uint32(uint256(ts.tokenId) >> 160); + inputs[8] = uint32(uint256(ts.tokenId) >> 128); + inputs[9] = uint32(uint256(ts.tokenId) >> 96); + inputs[10] = uint32(uint256(ts.tokenId) >> 64); + inputs[11] = uint32(uint256(ts.tokenId) >> 32); + inputs[12] = uint32(uint256(ts.tokenId)); + inputs[13] = uint256(ts.ercAddress); + inputs[14] = uint32(uint256(ts.recipientAddress) >> 224); + inputs[15] = uint32(uint256(ts.recipientAddress) >> 192); + inputs[16] = uint32(uint256(ts.recipientAddress) >> 160); + inputs[17] = uint32(uint256(ts.recipientAddress) >> 128); + inputs[18] = uint32(uint256(ts.recipientAddress) >> 96); + inputs[19] = uint32(uint256(ts.recipientAddress) >> 64); + inputs[20] = uint32(uint256(ts.recipientAddress) >> 32); + inputs[21] = uint32(uint256(ts.recipientAddress)); + inputs[22] = uint256(ts.commitments[0]); + inputs[23] = uint256(ts.commitments[1]); + inputs[24] = uint256(ts.nullifiers[0]); + inputs[25] = uint256(ts.nullifiers[1]); + inputs[26] = uint256(ts.compressedSecrets[0]); + inputs[27] = uint256(ts.compressedSecrets[1]); + + if (uint256(ts.transactionType) != 0) { + inputs[28] = uint256(roots[0]); + inputs[29] = uint256(roots[1]); } } - function getDepositInputs(Structures.Transaction calldata ts) - internal - pure - returns (uint256[] memory) - { - uint256[] memory inputs = new uint256[](4); - inputs[0] = uint256(ts.ercAddress); - inputs[1] = uint256(ts.tokenId); - inputs[2] = ts.value; - inputs[3] = uint256(ts.commitments[0]); - return inputs; - } - - function getSingleTransferInputs(Structures.Transaction calldata ts, uint256[2] memory roots) - internal - pure - returns (uint256[] memory) - { - uint256[] memory inputs = new uint256[](12); - inputs[0] = uint256(ts.ercAddress); - inputs[1] = uint256(ts.commitments[0]); - inputs[2] = uint256(ts.nullifiers[0]); - inputs[3] = roots[0]; - for (uint256 i = 4; i < 12; i++) { - inputs[i] = uint256(ts.compressedSecrets[i - 4]); - } - return inputs; - } - - function getWithdrawInputs(Structures.Transaction calldata ts, uint256[2] memory roots) - internal - pure - returns (uint256[] memory) - { - uint256[] memory inputs = new uint256[](6); - inputs[0] = uint256(ts.ercAddress); - inputs[1] = uint256(ts.tokenId); - inputs[2] = ts.value; - inputs[3] = uint256(ts.nullifiers[0]); - inputs[4] = uint256(ts.recipientAddress); - inputs[5] = roots[0]; - return inputs; - } - - function getDoubleTransferInputs(Structures.Transaction calldata ts, uint256[2] memory roots) - internal - pure - returns (uint256[] memory) - { - uint256[] memory inputs = new uint256[](16); - inputs[0] = uint256(ts.ercAddress); - inputs[1] = uint256(ts.ercAddress); - inputs[2] = uint256(ts.commitments[0]); - inputs[3] = uint256(ts.commitments[1]); - inputs[4] = uint256(ts.nullifiers[0]); - inputs[5] = uint256(ts.nullifiers[1]); - inputs[6] = roots[0]; - inputs[7] = roots[1]; - for (uint256 i = 8; i < 16; i++) { - inputs[i] = uint256(ts.compressedSecrets[i - 8]); - } - return inputs; - } - function calculateMerkleRoot(bytes32[] memory leaves) public pure returns (bytes32 result) { assembly { let length := mload(leaves) - let leavesPos := add(leaves,0x20) + let leavesPos := add(leaves, 0x20) let transactionHashesPos := mload(0x40) for { let i := 0 } lt(i, length) { i := add(i, 1) } { - mstore( - add(transactionHashesPos, mul(0x20, i)), - mload(add(leavesPos, mul(0x20, i))) - ) + mstore(add(transactionHashesPos, mul(0x20, i)), mload(add(leavesPos, mul(0x20, i)))) } for { let i := 5 @@ -211,10 +167,7 @@ library Utils { result := 0 } // returns bool if eq(and(iszero(left), iszero(right)), 0) { - result := keccak256( - add(transactionHashesPos, mul(mul(0x20, j), 2)), - 0x40 - ) + result := keccak256(add(transactionHashesPos, mul(mul(0x20, j), 2)), 0x40) } // returns bool mstore(add(transactionHashesPos, mul(0x20, j)), result) } diff --git a/nightfall-deployer/contracts/Verifier.sol b/nightfall-deployer/contracts/Verifier.sol index 2eda1c64d..5a0c6255a 100644 --- a/nightfall-deployer/contracts/Verifier.sol +++ b/nightfall-deployer/contracts/Verifier.sol @@ -27,121 +27,125 @@ Harry R pragma solidity ^0.8.0; -import "./Ownable.sol"; -import "./Pairing.sol"; +import './Ownable.sol'; +import './Pairing.sol'; library Verifier { + using Pairing for *; - using Pairing for *; + uint256 constant BN128_GROUP_ORDER = + 21888242871839275222246405745257275088548364400416034343698204186575808495617; - uint256 constant BN128_GROUP_ORDER = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + struct Proof_G16 { + Pairing.G1Point A; + Pairing.G2Point B; + Pairing.G1Point C; + } - struct Proof_G16 { - Pairing.G1Point A; - Pairing.G2Point B; - Pairing.G1Point C; - } - - struct Verification_Key_G16 { - Pairing.G1Point alpha; + struct Verification_Key_G16 { + Pairing.G1Point alpha; Pairing.G2Point beta; Pairing.G2Point gamma; Pairing.G2Point delta; Pairing.G1Point[] gamma_abc; - } - - function verify(uint256[] memory _proof, uint256[] memory _publicInputs, uint256[] memory _vk) public returns (bool result) { - if (verificationCalculation(_proof, _publicInputs, _vk) == 0) { - result = true; - } else { - result = false; - } - } - - function verificationCalculation(uint256[] memory _proof, uint256[] memory _publicInputs, uint256[] memory _vk) public returns (uint) { - - Proof_G16 memory proof; - Pairing.G1Point memory vk_dot_inputs; - Verification_Key_G16 memory vk; - - vk_dot_inputs = Pairing.G1Point(0, 0); //initialise - - proof.A = Pairing.G1Point(_proof[0], _proof[1]); - proof.B = Pairing.G2Point([_proof[2], _proof[3]], [_proof[4], _proof[5]]); - proof.C = Pairing.G1Point(_proof[6], _proof[7]); - - vk.alpha = Pairing.G1Point(_vk[0],_vk[1]); - vk.beta = Pairing.G2Point([_vk[2],_vk[3]],[_vk[4],_vk[5]]); - vk.gamma = Pairing.G2Point([_vk[6],_vk[7]],[_vk[8],_vk[9]]); - vk.delta = Pairing.G2Point([_vk[10],_vk[11]],[_vk[12],_vk[13]]); - - if (_vk.length > 14) { - vk.gamma_abc = new Pairing.G1Point[]((_vk.length - 14)/2); // num public inputs + 1 - for (uint i = 14; i < _vk.length; i+=2) { - vk.gamma_abc[(i-14)/2] = Pairing.G1Point( - _vk[i], _vk[i+1] - ); + } + + function verify( + uint256[] memory _proof, + uint256[] memory _publicInputs, + uint256[] memory _vk + ) public returns (bool result) { + if (verificationCalculation(_proof, _publicInputs, _vk) == 0) { + result = true; + } else { + result = false; + } + } + + function verificationCalculation( + uint256[] memory _proof, + uint256[] memory _publicInputs, + uint256[] memory _vk + ) public returns (uint256) { + Proof_G16 memory proof; + Pairing.G1Point memory vk_dot_inputs; + Verification_Key_G16 memory vk; + + vk_dot_inputs = Pairing.G1Point(0, 0); //initialise + + proof.A = Pairing.G1Point(_proof[0], _proof[1]); + proof.B = Pairing.G2Point([_proof[2], _proof[3]], [_proof[4], _proof[5]]); + proof.C = Pairing.G1Point(_proof[6], _proof[7]); + + vk.alpha = Pairing.G1Point(_vk[0], _vk[1]); + vk.beta = Pairing.G2Point([_vk[2], _vk[3]], [_vk[4], _vk[5]]); + vk.gamma = Pairing.G2Point([_vk[6], _vk[7]], [_vk[8], _vk[9]]); + vk.delta = Pairing.G2Point([_vk[10], _vk[11]], [_vk[12], _vk[13]]); + + if (_vk.length > 14) { + vk.gamma_abc = new Pairing.G1Point[]((_vk.length - 14) / 2); // num public inputs + 1 + for (uint256 i = 14; i < _vk.length; i += 2) { + vk.gamma_abc[(i - 14) / 2] = Pairing.G1Point(_vk[i], _vk[i + 1]); + } } - } - - - /* require(vk.gamma.abc.length == 2, "Length of vk.gamma.abc is incorrect!"); */ - // Replacing for the above require statement so that the proof verification returns false. Removing require statements to ensure a wrong proof verification challenge's require statement correctly works - if (vk.gamma_abc.length != _publicInputs.length + 1) { - return 1; - } - + /* require(vk.gamma.abc.length == 2, "Length of vk.gamma.abc is incorrect!"); */ + // Replacing for the above require statement so that the proof verification returns false. Removing require statements to ensure a wrong proof verification challenge's require statement correctly works + if (vk.gamma_abc.length != _publicInputs.length + 1) { + return 1; + } + { + Pairing.G1Point memory sm_qpih; + // The following success variables replace require statements with corresponding functions called. Removing require statements to ensure a wrong proof verification challenge's require statement correctly works + bool success_sm_qpih; + bool success_vkdi_sm_qpih; + for (uint256 i = 0; i < _publicInputs.length; i++) { + // check for overflow attacks + if (_publicInputs[i] >= BN128_GROUP_ORDER) return 2; + (sm_qpih, success_sm_qpih) = Pairing.scalar_mul( + vk.gamma_abc[i + 1], + _publicInputs[i] + ); + (vk_dot_inputs, success_vkdi_sm_qpih) = Pairing.addition(vk_dot_inputs, sm_qpih); + if (!success_sm_qpih || !success_vkdi_sm_qpih) { + return 2; + } + } + } + { + // The following success variables replace require statements with corresponding functions called. Removing require statements to ensure a wrong proof verification challenge's require statement correctly works + bool success_vkdi_q; + (vk_dot_inputs, success_vkdi_q) = Pairing.addition(vk_dot_inputs, vk.gamma_abc[0]); + if (!success_vkdi_q) { + return 3; + } + } - { - Pairing.G1Point memory sm_qpih; - // The following success variables replace require statements with corresponding functions called. Removing require statements to ensure a wrong proof verification challenge's require statement correctly works - bool success_sm_qpih; - bool success_vkdi_sm_qpih; - for (uint i = 0; i < _publicInputs.length; i++) { - // check for overflow attacks - if (_publicInputs[i] >= BN128_GROUP_ORDER) return 2; - (sm_qpih, success_sm_qpih) = Pairing.scalar_mul(vk.gamma_abc[i+1], _publicInputs[i]); - (vk_dot_inputs, success_vkdi_sm_qpih) = Pairing.addition( - vk_dot_inputs, - sm_qpih + /** + * e(A*G^{alpha}, B*H^{beta}) = e(G^{alpha}, H^{beta}) * e(G^{psi}, H^{gamma}) + * * e(C, H) + * where psi = \sum_{i=0}^l input_i pvk.query[i] + */ + { + // The following success variables replace require statements with corresponding functions called. Removing require statements to ensure a wrong proof verification challenge's require statement correctly works + bool success_pp4_out_not_0; + bool success_pp4_pairing; + (success_pp4_out_not_0, success_pp4_pairing) = Pairing.pairingProd4( + proof.A, + proof.B, + Pairing.negate(vk_dot_inputs), + vk.gamma, + Pairing.negate(proof.C), + vk.delta, + Pairing.negate(vk.alpha), + vk.beta ); - if (!success_sm_qpih || !success_vkdi_sm_qpih) { - return 2; - } - } - } - - { - // The following success variables replace require statements with corresponding functions called. Removing require statements to ensure a wrong proof verification challenge's require statement correctly works - bool success_vkdi_q; - (vk_dot_inputs, success_vkdi_q) = Pairing.addition(vk_dot_inputs, vk.gamma_abc[0]); - if (!success_vkdi_q) { - return 3; - } - } - - - /** - * e(A*G^{alpha}, B*H^{beta}) = e(G^{alpha}, H^{beta}) * e(G^{psi}, H^{gamma}) - * * e(C, H) - * where psi = \sum_{i=0}^l input_i pvk.query[i] - */ - { - // The following success variables replace require statements with corresponding functions called. Removing require statements to ensure a wrong proof verification challenge's require statement correctly works - bool success_pp4_out_not_0; - bool success_pp4_pairing; - (success_pp4_out_not_0, success_pp4_pairing) = Pairing.pairingProd4( - proof.A, proof.B, - Pairing.negate(vk_dot_inputs), vk.gamma, - Pairing.negate(proof.C), vk.delta, - Pairing.negate(vk.alpha), vk.beta); - if (!success_pp4_out_not_0 || !success_pp4_pairing) { - return 5; + if (!success_pp4_out_not_0 || !success_pp4_pairing) { + return 5; + } } - } - return 0; - } + return 0; + } } diff --git a/nightfall-optimist/src/routes/proposer.mjs b/nightfall-optimist/src/routes/proposer.mjs index 0ced53bc8..b087ab2ef 100644 --- a/nightfall-optimist/src/routes/proposer.mjs +++ b/nightfall-optimist/src/routes/proposer.mjs @@ -365,8 +365,7 @@ router.post('/offchain-transaction', async (req, res) => { try { switch (Number(transactionType)) { case 1: - case 2: - case 3: { + case 2: { // When comparing this with getTransactionSubmittedCalldata, // note we dont need to decompressProof as proofs are only compressed if they go on-chain. // let's not directly call transactionSubmittedEventHandler, instead, we'll queue it diff --git a/nightfall-optimist/src/services/challenges.mjs b/nightfall-optimist/src/services/challenges.mjs index 6d7f0e809..5bbe0fe3a 100644 --- a/nightfall-optimist/src/services/challenges.mjs +++ b/nightfall-optimist/src/services/challenges.mjs @@ -1,10 +1,10 @@ import WebSocket from 'ws'; import config from 'config'; -import { rand } from 'common-files/utils/crypto/crypto-random.mjs'; import logger from 'common-files/utils/logger.mjs'; import Web3 from 'common-files/utils/web3.mjs'; import { getContractInstance } from 'common-files/utils/contract.mjs'; import constants from 'common-files/constants/index.mjs'; +import { rand } from 'common-files/utils/crypto/crypto-random.mjs'; import { getBlockByBlockHash, getBlockByTransactionHash, @@ -152,22 +152,6 @@ export async function createChallenge(block, transactions, err) { .encodeABI(); break; } - // invalid transaction type - case 2: { - const { transactionHashIndex: transactionIndex } = err.metadata; - // Create a challenge - txDataToSign = await challengeContractInstance.methods - .challengeTransactionType( - Block.buildSolidityStruct(block), - transactions.map(t => Transaction.buildSolidityStruct(t)), - transactionIndex, - salt, - ) - .encodeABI(); - logger.debug('returning raw transaction'); - logger.silly(`raw transaction is ${JSON.stringify(txDataToSign, null, 2)}`); - break; - } // historic root is incorrect case 3: { const { transactionHashIndex: transactionIndex } = err.metadata; @@ -187,63 +171,37 @@ export async function createChallenge(block, transactions, err) { const { transactionHashIndex: transactionIndex } = err.metadata; // Create a challenge const uncompressedProof = transactions[transactionIndex].proof; - if (transactions[transactionIndex].transactionType === '0') { - txDataToSign = await challengeContractInstance.methods - .challengeProofVerification( - Block.buildSolidityStruct(block), - transactions.map(t => Transaction.buildSolidityStruct(t)), - transactionIndex, - uncompressedProof, - salt, - ) - .encodeABI(); - } else if (transactions[transactionIndex].transactionType === '2') { - // Create a specific challenge for a double_transfer - const [historicInput1, historicInput2] = await Promise.all( - transactions[transactionIndex].historicRootBlockNumberL2.map(async b => { - const historicBlock = await getBlockByBlockNumberL2(b); - const historicTxs = await getTransactionsByTransactionHashes(block.transactionHashes); + const [historicInput1, historicInput2] = await Promise.all( + transactions[transactionIndex].historicRootBlockNumberL2.map(async (b, i) => { + if (transactions[transactionIndex].nullifiers[i] === 0) { return { - historicBlock, - historicTxs, + historicBlock: {}, + historicTxs: [], }; - }), - ); - txDataToSign = await challengeContractInstance.methods - .challengeProofVerification( - Block.buildSolidityStruct(block), - transactions.map(t => Transaction.buildSolidityStruct(t)), - transactionIndex, - Block.buildSolidityStruct(historicInput1.historicBlock), - Block.buildSolidityStruct(historicInput2.historicBlock), + } + const historicBlock = await getBlockByBlockNumberL2(b); + const historicTxs = await getTransactionsByTransactionHashes(block.transactionHashes); + return { + historicBlock: Block.buildSolidityStruct(historicBlock), + historicTxs, + }; + }), + ); + + txDataToSign = await challengeContractInstance.methods + .challengeProofVerification( + Block.buildSolidityStruct(block), + transactions.map(t => Transaction.buildSolidityStruct(t)), + transactionIndex, + [historicInput1.historicBlock, historicInput2.historicBlock], + [ historicInput1.historicTxs.map(t => Transaction.buildSolidityStruct(t)), historicInput2.historicTxs.map(t => Transaction.buildSolidityStruct(t)), - uncompressedProof, - salt, - ) - .encodeABI(); - } else { - const blockL2ContainingHistoricRoot = await getBlockByBlockNumberL2( - transactions[transactionIndex].historicRootBlockNumberL2[0], // TODO - ); - const transactionsOfblockL2ContainingHistoricRoot = - await getTransactionsByTransactionHashes( - blockL2ContainingHistoricRoot.transactionHashes, - ); - txDataToSign = await challengeContractInstance.methods - .challengeProofVerification( - Block.buildSolidityStruct(block), - transactions.map(t => Transaction.buildSolidityStruct(t)), - transactionIndex, - Block.buildSolidityStruct(blockL2ContainingHistoricRoot), - transactionsOfblockL2ContainingHistoricRoot.map(t => - Transaction.buildSolidityStruct(t), - ), - uncompressedProof, - salt, - ) - .encodeABI(); - } + ], + uncompressedProof, + salt, + ) + .encodeABI(); break; } // Challenge Duplicate Nullfier diff --git a/nightfall-optimist/src/services/transaction-checker.mjs b/nightfall-optimist/src/services/transaction-checker.mjs index 58b14a11f..eeeb9e27a 100644 --- a/nightfall-optimist/src/services/transaction-checker.mjs +++ b/nightfall-optimist/src/services/transaction-checker.mjs @@ -14,9 +14,9 @@ import { waitForContract } from '../event-handlers/subscribe.mjs'; import { getBlockByBlockNumberL2 } from './database.mjs'; import verify from './verify.mjs'; -const { generalise, GN } = gen; -const { PROVING_SCHEME, BACKEND, CURVE, BN128_GROUP_ORDER, MAX_PUBLIC_VALUES } = config; const { ZERO, CHALLENGES_CONTRACT_NAME } = constants; +const { generalise } = gen; +const { PROVING_SCHEME, BACKEND, CURVE, BN128_GROUP_ORDER, MAX_PUBLIC_VALUES } = config; function isOverflow(value, check) { const bigValue = value.bigInt; @@ -35,108 +35,22 @@ async function checkTransactionHash(transaction) { throw new TransactionError('The transaction hash did not match the transaction data', 0); } } -// next that the fields provided are consistent with the transaction type -async function checkTransactionType(transaction) { - switch (Number(transaction.transactionType)) { - // Assuming nullifiers and commitments can't be valid ZEROs. - // But points can such as compressedSecrets, Proofs - case 0: // deposit - if ( - (Number(transaction.tokenType) !== 0 && - transaction.tokenId === ZERO && - BigInt(transaction.value) === 0n) || - transaction.ercAddress === ZERO || - transaction.recipientAddress !== ZERO || - transaction.commitments[0] === ZERO || - transaction.commitments[1] !== ZERO || - transaction.commitments.length !== 2 || - transaction.nullifiers.some(n => n !== ZERO) || - transaction.compressedSecrets.some(cs => cs !== ZERO) || - transaction.compressedSecrets.length !== 2 || - transaction.proof.every(p => p === ZERO) || - // This extra check is unique to deposits - Number(transaction.historicRootBlockNumberL2[0]) !== 0 || - Number(transaction.historicRootBlockNumberL2[1]) !== 0 - ) - throw new TransactionError( - 'The data provided was inconsistent with a transaction type of DEPOSIT', - 1, - ); - break; - case 1: // single token transaction - if ( - BigInt(transaction.value) !== 0n || - transaction.commitments[0] === ZERO || - transaction.commitments[1] !== ZERO || - transaction.commitments.length !== 2 || - transaction.nullifiers[0] === ZERO || - transaction.nullifiers[1] !== ZERO || - transaction.nullifiers.length !== 2 || - transaction.compressedSecrets.every(cs => cs === ZERO) || - transaction.compressedSecrets.length !== 2 || - transaction.proof.every(p => p === ZERO) - ) - throw new TransactionError( - 'The data provided was inconsistent with a transaction type of SINGLE_TRANSFER', - 1, - ); - break; - case 2: // double token transaction - if ( - BigInt(transaction.value) !== 0n || - transaction.commitments.some(c => c === ZERO) || - transaction.commitments.length !== 2 || - transaction.nullifiers.some(n => n === ZERO) || - transaction.nullifiers.length !== 2 || - transaction.nullifiers[0] === transaction.nullifiers[1] || - transaction.compressedSecrets.every(cs => cs === ZERO) || - transaction.compressedSecrets.length !== 2 || - transaction.proof.every(p => p === ZERO) - ) - throw new TransactionError( - 'The data provided was inconsistent with a transaction type of DOUBLE_TRANSFER', - 1, - ); - break; - case 3: // withdraw transaction - if ( - (Number(transaction.tokenType) !== 0 && - transaction.tokenId === ZERO && - BigInt(transaction.value) === 0n) || - transaction.ercAddress === ZERO || - transaction.recipientAddress === ZERO || - transaction.commitments.some(c => c !== ZERO) || - transaction.nullifiers[0] === ZERO || - transaction.nullifiers[1] !== ZERO || - transaction.nullifiers.length !== 2 || - transaction.compressedSecrets.some(cs => cs !== ZERO) || - transaction.proof.every(p => p === ZERO) - ) - throw new TransactionError( - 'The data provided was inconsistent with a transaction type of WITHDRAW', - 1, - ); - break; - default: - throw new TransactionError('Unknown transaction type', 2); - } -} async function checkHistoricRoot(transaction) { // Deposit transaction have a historic root of 0 // the validity is tested in checkTransactionType - if (Number(transaction.transactionType) === 1 || Number(transaction.transactionType) === 3) { + if (Number(transaction.nullifiers[0]) !== 0) { const historicRootFirst = await getBlockByBlockNumberL2( transaction.historicRootBlockNumberL2[0], ); if (historicRootFirst === null) throw new TransactionError('The historic root in the transaction does not exist', 3); } - if (Number(transaction.transactionType) === 2) { - const [historicRootFirst, historicRootSecond] = await Promise.all( - transaction.historicRootBlockNumberL2.map(h => getBlockByBlockNumberL2(h)), + if (Number(transaction.nullifiers[1]) !== 0) { + const historicRootSecond = await getBlockByBlockNumberL2( + transaction.historicRootBlockNumberL2[1], ); - if (historicRootFirst === null || historicRootSecond === null) + if (historicRootSecond === null) throw new TransactionError('The historic root in the transaction does not exist', 3); } } @@ -149,105 +63,52 @@ async function verifyProof(transaction) { .call(); // to verify a proof, we make use of a zokrates-worker, which has an offchain // verifier capability - let inputs; - const historicRootFirst = (await getBlockByBlockNumberL2( - transaction.historicRootBlockNumberL2[0], - )) ?? { root: ZERO }; - const historicRootSecond = (await getBlockByBlockNumberL2( - transaction.historicRootBlockNumberL2[1], - )) ?? { root: ZERO }; + const historicRootFirst = + transaction.nullifiers[0] === ZERO + ? { root: ZERO } + : (await getBlockByBlockNumberL2(transaction.historicRootBlockNumberL2[0])) ?? { root: ZERO }; + const historicRootSecond = + transaction.nullifiers[1] === ZERO + ? { root: ZERO } + : (await getBlockByBlockNumberL2(transaction.historicRootBlockNumberL2[1])) ?? { root: ZERO }; - const bin = new GN(transaction.recipientAddress).binary.padStart(256, '0'); - const parity = bin[0]; - const ordinate = bin.slice(1); - const binaryEPub = [parity, new GN(ordinate, 'binary').field(BN128_GROUP_ORDER, false)]; + const inputs = generalise( + [ + transaction.value, + transaction.historicRootBlockNumberL2, + transaction.transactionType, + transaction.tokenType, + generalise(transaction.tokenId).limbs(32, 8), + transaction.ercAddress, + generalise(transaction.recipientAddress).limbs(32, 8), + transaction.commitments, + transaction.nullifiers, + transaction.compressedSecrets, + ].flat(Infinity), + ).all.hex(32); - switch (Number(transaction.transactionType)) { - case 0: // deposit transaction - inputs = generalise( - [ - transaction.ercAddress, - generalise(transaction.tokenId).limbs(32, 8), - transaction.value, - transaction.commitments[0], - ].flat(Infinity), - ); - if ( - isOverflow(transaction.ercAddress, MAX_PUBLIC_VALUES.ERCADDRESS) || - isOverflow(transaction.commitments[0], MAX_PUBLIC_VALUES.COMMITMENTS) - ) - throw new TransactionError('Truncated value overflow in public input', 4); - break; - case 1: // single transfer transaction - inputs = generalise( - [ - transaction.commitments[0], - transaction.nullifiers[0], - historicRootFirst.root, - binaryEPub, - transaction.ercAddress, - transaction.tokenId, - ...transaction.compressedSecrets, - ].flat(Infinity), - ); - // check for truncation overflow attacks - if ( - isOverflow(transaction.commitments[0], MAX_PUBLIC_VALUES.COMMITMENTS) || - isOverflow(transaction.nullifiers[0], MAX_PUBLIC_VALUES.NULLIFIER) || - isOverflow(historicRootFirst.root, BN128_GROUP_ORDER) - ) - throw new TransactionError('Overflow in public input', 4); - break; - case 2: // double transfer transaction - inputs = generalise( - [ - transaction.commitments, // not truncating here as we already ensured hash < group order - transaction.nullifiers, - historicRootFirst.root, - historicRootSecond.root, - binaryEPub, - transaction.ercAddress, - transaction.tokenId, - ...transaction.compressedSecrets, - ].flat(Infinity), - ); - // check for truncation overflow attacks - for (let i = 0; i < transaction.nullifiers.length; i++) { - if (isOverflow(transaction.nullifiers[i], MAX_PUBLIC_VALUES.NULLIFIER)) - throw new TransactionError('Overflow in public input', 4); - } - for (let i = 0; i < transaction.commitments.length; i++) { - if (isOverflow(transaction.commitments[i], MAX_PUBLIC_VALUES.COMMITMENT)) - throw new TransactionError('Overflow in public input', 4); - } - if ( - isOverflow(historicRootFirst.root, BN128_GROUP_ORDER) || - isOverflow(historicRootSecond.root, BN128_GROUP_ORDER) - ) - throw new TransactionError('Overflow in public input', 4); - break; - case 3: // withdraw transaction - inputs = generalise( - [ - transaction.ercAddress, - generalise(transaction.tokenId).limbs(32, 8), - transaction.value, - transaction.nullifiers[0], - transaction.recipientAddress, - historicRootFirst.root, - ].flat(Infinity), - ); - // check for truncation overflow attacks - if ( - isOverflow(transaction.ercAddress, MAX_PUBLIC_VALUES.ERCADDRESS) || - isOverflow(transaction.recipientAddress, MAX_PUBLIC_VALUES.ERCADDRESS) || - isOverflow(transaction.nullifiers[0], MAX_PUBLIC_VALUES.NULLIFIER) || - isOverflow(historicRootFirst.root, BN128_GROUP_ORDER) - ) - throw new TransactionError('Truncated value overflow in public input', 4); - break; - default: - throw new TransactionError('Unknown transaction type', 2); + if (Number(transaction.transactionType) !== 0) { + inputs.push(generalise(historicRootFirst.root).hex(32)); + inputs.push(generalise(historicRootSecond.root).hex(32)); + } + + if ( + isOverflow(transaction.ercAddress, MAX_PUBLIC_VALUES.ERCADDRESS) || + isOverflow(historicRootFirst.root, BN128_GROUP_ORDER) || + isOverflow(historicRootSecond.root, BN128_GROUP_ORDER) || + (transaction.transactionType === 2 && + isOverflow(transaction.recipientAddress, MAX_PUBLIC_VALUES.ERCADDRESS)) + ) { + throw new TransactionError('Overflow in public input', 4); + } + + for (let i = 0; i < transaction.nullifiers.length; i++) { + if (isOverflow(transaction.nullifiers[i], MAX_PUBLIC_VALUES.NULLIFIER)) + throw new TransactionError('Overflow in public input', 4); + } + for (let i = 0; i < transaction.commitments.length; i++) { + if (isOverflow(transaction.commitments[i], MAX_PUBLIC_VALUES.COMMITMENT)) + throw new TransactionError('Overflow in public input', 4); } // check for modular overflow attacks // if (inputs.filter(input => input.bigInt >= BN128_GROUP_ORDER).length > 0) @@ -258,7 +119,7 @@ async function verifyProof(transaction) { provingScheme: PROVING_SCHEME, backend: BACKEND, curve: CURVE, - inputs: inputs.all.hex(32), + inputs, }); if (!res) throw new TransactionError('The proof did not verify', 4); } @@ -266,7 +127,6 @@ async function verifyProof(transaction) { async function checkTransaction(transaction) { return Promise.all([ checkTransactionHash(transaction), - checkTransactionType(transaction), checkHistoricRoot(transaction), verifyProof(transaction), ]); diff --git a/package.json b/package.json index 68a44b76c..662a4e037 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "neg-test-ropsten": "mocha --timeout 0 --bail --exit test/neg-http.mjs", "test-e2e-protocol": "LOG_LEVEL=error mocha --timeout 0 --bail --exit test/e2e/protocol/*.test.mjs ", "test-gas": "mocha --timeout 0 --bail --exit test/e2e/gas.test.mjs ", + "test-circuits": "LOG_LEVEL=error mocha --timeout 0 --bail --exit test/e2e/circuits.test.mjs ", "test-e2e-tokens": "LOG_LEVEL=error mocha --timeout 0 --bail --exit test/e2e/tokens/*.test.mjs ", "test-erc20-tokens": "LOG_LEVEL=error mocha --timeout 0 --bail --exit test/e2e/tokens/erc20.test.mjs ", "test-erc721-tokens": "LOG_LEVEL=error mocha --timeout 0 --bail --exit test/e2e/tokens/erc721.test.mjs ", diff --git a/test/e2e/circuits.test.mjs b/test/e2e/circuits.test.mjs new file mode 100644 index 000000000..54b2b38e7 --- /dev/null +++ b/test/e2e/circuits.test.mjs @@ -0,0 +1,234 @@ +/* eslint-disable no-await-in-loop */ +import chai from 'chai'; +import chaiHttp from 'chai-http'; +import chaiAsPromised from 'chai-as-promised'; +import config from 'config'; +import logger from 'common-files/utils/logger.mjs'; +import Nf3 from '../../cli/lib/nf3.mjs'; +import { expectTransaction, depositNTransactions, Web3Client } from '../utils.mjs'; + +// so we can use require with mjs file +const { expect } = chai; +chai.use(chaiHttp); +chai.use(chaiAsPromised); + +const environment = config.ENVIRONMENTS[process.env.ENVIRONMENT] || config.ENVIRONMENTS.localhost; + +const { + fee, + txPerBlock, + tokenConfigs: { tokenType, tokenId }, + mnemonics, + signingKeys, +} = config.TEST_OPTIONS; + +const nf3Users = [new Nf3(signingKeys.user1, environment)]; +const nf3Proposer = new Nf3(signingKeys.proposer1, environment); + +const web3Client = new Web3Client(); + +let erc20Address; +let stateAddress; +let eventLogs = []; + +/* + This function tries to zero the number of unprocessed transactions in the optimist node + that nf3 is connected to. We call it extensively on the tests, as we want to query stuff from the + L2 layer, which is dependent on a block being made. We also need 0 unprocessed transactions by the end + of the tests, otherwise the optimist will become out of sync with the L2 block count on-chain. +*/ +describe('General Circuit Test', () => { + before(async () => { + await nf3Proposer.init(mnemonics.proposer); + // we must set the URL from the point of view of the client container + await nf3Proposer.registerProposer('http://optimist1'); + + // Proposer listening for incoming events + const newGasBlockEmitter = await nf3Proposer.startProposer(); + newGasBlockEmitter.on('gascost', async gasUsed => { + logger.debug( + `Block proposal gas cost was ${gasUsed}, cost per transaction was ${gasUsed / txPerBlock}`, + ); + }); + + await nf3Users[0].init(mnemonics.user1); + erc20Address = await nf3Users[0].getContractAddress('ERC20Mock'); + + stateAddress = await nf3Users[0].stateContractAddress; + web3Client.subscribeTo('logs', eventLogs, { address: stateAddress }); + + await nf3Users[0].makeBlockNow(); + }); + + it('Test that all circuits are working', async () => { + async function getBalance() { + return (await nf3Users[0].getLayer2Balances())[erc20Address]?.[0].balance || 0; + } + + logger.debug(`Sending 1 deposit of 10...`); + await depositNTransactions(nf3Users[0], 1, erc20Address, tokenType, 10, tokenId, fee); + await nf3Users[0].makeBlockNow(); + + // Wait until we see the right number of blocks appear + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + + // Deposit checks here + + logger.debug(`Sending single transfer with no change...`); + const singleTransferNoChange = await nf3Users[0].transfer( + false, + erc20Address, + tokenType, + 10, + tokenId, + nf3Users[0].zkpKeys.compressedZkpPublicKey, + fee, + ); + expectTransaction(singleTransferNoChange); + + await nf3Users[0].makeBlockNow(); + + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + + // Single Transfer No Change checks here + + logger.debug(`Sending single transfer with change...`); + const singleTransferChange = await nf3Users[0].transfer( + false, + erc20Address, + tokenType, + 5, + tokenId, + nf3Users[0].zkpKeys.compressedZkpPublicKey, + fee, + ); + expectTransaction(singleTransferChange); + + await nf3Users[0].makeBlockNow(); + + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + + logger.debug(`Sending withdrawal with no change...`); + const withdrawalNoChange = await nf3Users[0].withdraw( + false, + erc20Address, + tokenType, + 5, + tokenId, + nf3Users[0].ethereumAddress, + ); + + expectTransaction(withdrawalNoChange); + + await nf3Users[0].makeBlockNow(); + + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + // Withdrawal No Change checks here + + logger.debug(`Sending withdrawal with change...`); + const withdrawalChange = await nf3Users[0].withdraw( + false, + erc20Address, + tokenType, + 2, + tokenId, + nf3Users[0].ethereumAddress, + ); + + expectTransaction(withdrawalChange); + + await nf3Users[0].makeBlockNow(); + + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + // Withdrawal Change checks here + + logger.debug(`Sending deposit of 8...`); + await depositNTransactions(nf3Users[0], 1, erc20Address, tokenType, 8, tokenId, fee); + await nf3Users[0].makeBlockNow(); + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + + logger.debug(`Sending double transfer with change...`); + const doubleTransferChange = await nf3Users[0].transfer( + false, + erc20Address, + tokenType, + 9, + tokenId, + nf3Users[0].zkpKeys.compressedZkpPublicKey, + fee, + ); + + expectTransaction(doubleTransferChange); + + await nf3Users[0].makeBlockNow(); + + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + // Double transfer Change checks here + + logger.debug(`Sending double transfer with no change...`); + const doubleTransferNoChange = await nf3Users[0].transfer( + false, + erc20Address, + tokenType, + 11, + tokenId, + nf3Users[0].zkpKeys.compressedZkpPublicKey, + fee, + ); + + expectTransaction(doubleTransferNoChange); + + await nf3Users[0].makeBlockNow(); + + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + // Double transfer No Change checks here + + logger.debug(`Sending deposit of 4...`); + await depositNTransactions(nf3Users[0], 1, erc20Address, tokenType, 4, tokenId, fee); + await nf3Users[0].makeBlockNow(); + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + + logger.debug(`Sending double withdrawal with change...`); + const doubleWithdrawalChange = await nf3Users[0].withdraw( + false, + erc20Address, + tokenType, + 12, + tokenId, + nf3Users[0].ethereumAddress, + ); + + expectTransaction(doubleWithdrawalChange); + + await nf3Users[0].makeBlockNow(); + + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + // Double Withdrawal Change checks here + + logger.debug(`Sending deposit of 2...`); + await depositNTransactions(nf3Users[0], 1, erc20Address, tokenType, 2, tokenId, fee); + await nf3Users[0].makeBlockNow(); + + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + + logger.debug(`Sending double Withdrawal with no change...`); + const doubleWithdrawalNoChange = await nf3Users[0].withdraw( + false, + erc20Address, + tokenType, + 5, + tokenId, + nf3Users[0].ethereumAddress, + ); + + expectTransaction(doubleWithdrawalNoChange); + + await nf3Users[0].makeBlockNow(); + + eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); + // Double Withdrawal No Change + + const finalBalance = await getBalance(); + expect(finalBalance).to.be.equal(0); + }); +}); diff --git a/test/e2e/gas.test.mjs b/test/e2e/gas.test.mjs index 92dd837be..a4cbec193 100644 --- a/test/e2e/gas.test.mjs +++ b/test/e2e/gas.test.mjs @@ -10,6 +10,7 @@ import { withdrawNTransactions, Web3Client, expectTransaction, + waitForTimeout, } from '../utils.mjs'; // so we can use require with mjs file @@ -42,40 +43,6 @@ let eventLogs = []; const averageL1GasCost = receipts => receipts.map(receipt => receipt.gasUsed).reduce((acc, el) => acc + el) / receipts.length; -/* - This function tries to zero the number of unprocessed transactions in the optimist node - that nf3 is connected to. We call it extensively on the tests, as we want to query stuff from the - L2 layer, which is dependent on a block being made. We also need 0 unprocessed transactions by the end - of the tests, otherwise the optimist will become out of sync with the L2 block count on-chain. -*/ -const emptyL2 = async nf3Instance => { - let count = await nf3Instance.unprocessedTransactionCount(); - while (count !== 0) { - if (count % txPerBlock) { - const tx = (count % txPerBlock) - 1; - for (let i = 0; i < tx; i++) { - eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); - } - } else { - const tx = txPerBlock - count; - - await depositNTransactions( - nf3Instance, - tx, - erc20Address, - tokenType, - transferValue, - tokenId, - fee, - ); - - eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); - - count = await nf3Instance.unprocessedTransactionCount(); - } - } -}; - describe('Gas test', () => { let gasCost = 0; before(async () => { @@ -194,6 +161,7 @@ describe('Gas test', () => { nf3Users[0].ethereumAddress, fee, ); + await nf3Users[0].makeBlockNow(); eventLogs = await web3Client.waitForEvent(eventLogs, ['blockProposed']); expect(gasCost).to.be.lessThan(expectedGasCostPerTx); console.log('Withdraw L1 average gas used, if on-chain, was', averageL1GasCost(receipts)); @@ -204,9 +172,11 @@ describe('Gas test', () => { it('should withdraw from L2, checking for L1 balance (only with time-jump client)', async function () { const nodeInfo = await web3Client.getInfo(); if (nodeInfo.includes('TestRPC')) { + waitForTimeout(10000); const startBalance = await web3Client.getBalance(nf3Users[0].ethereumAddress); const withdrawal = await nf3Users[0].getLatestWithdrawHash(); - await emptyL2(nf3Users[0]); + await nf3Users[0].makeBlockNow(); + await web3Client.waitForEvent(eventLogs, ['blockProposed']); await web3Client.timeJump(3600 * 24 * 10); // jump in time by 10 days const commitments = await nf3Users[0].getPendingWithdraws(); expect( @@ -230,7 +200,6 @@ describe('Gas test', () => { }); after(async () => { - await emptyL2(nf3Users[0]); await nf3Proposer1.deregisterProposer(); await nf3Proposer1.close(); await nf3Users[0].close(); diff --git a/test/e2e/tokens/erc20.test.mjs b/test/e2e/tokens/erc20.test.mjs index 226e828c2..4db6663a9 100644 --- a/test/e2e/tokens/erc20.test.mjs +++ b/test/e2e/tokens/erc20.test.mjs @@ -271,7 +271,7 @@ describe('ERC20 tests', () => { false, erc20Address, tokenType, - transferValue, + transferValue / 2, tokenId, nf3Users[0].ethereumAddress, ); @@ -289,7 +289,7 @@ describe('ERC20 tests', () => { false, erc20Address, tokenType, - transferValue, + Math.floor(transferValue / 2), tokenId, nf3Users[0].ethereumAddress, ); @@ -318,7 +318,7 @@ describe('ERC20 tests', () => { false, erc20Address, tokenType, - transferValue, + Math.floor(transferValue / 2), tokenId, nf3Users[0].ethereumAddress, ); @@ -349,6 +349,23 @@ describe('ERC20 tests', () => { this.skip(); } }); + + it('should withdraw from L2 with some change', async function () { + const beforeBalance = (await nf3Users[0].getLayer2Balances())[erc20Address]?.[0].balance; + const rec = await nf3Users[0].withdraw( + false, + erc20Address, + tokenType, + Math.floor(transferValue / 2), + tokenId, + nf3Users[0].ethereumAddress, + ); + expectTransaction(rec); + + logger.debug(` Gas used was ${Number(rec.gasUsed)}`); + const afterBalance = (await nf3Users[0].getLayer2Balances())[erc20Address]?.[0].balance; + expect(afterBalance).to.be.lessThan(beforeBalance); + }); }); describe('Instant withdrawals from L2', () => { @@ -361,7 +378,7 @@ describe('ERC20 tests', () => { nf3LiquidityProvider.ethereumAddress, nf3LiquidityProvider.shieldContractAddress, tokenType, - transferValue, + Math.floor(transferValue / 2), web3Client.getWeb3(), !!nf3LiquidityProvider.ethereumSigningKey, ); @@ -392,7 +409,7 @@ describe('ERC20 tests', () => { false, erc20Address, tokenType, - transferValue, + Math.floor(transferValue / 2), tokenId, nf3Users[0].ethereumAddress, fee, @@ -423,7 +440,7 @@ describe('ERC20 tests', () => { false, erc20Address, tokenType, - transferValue, + Math.floor(transferValue / 2), tokenId, nf3Users[0].ethereumAddress, fee, @@ -537,7 +554,6 @@ describe('ERC20 tests', () => { await new Promise(resolve => setTimeout(resolve, 30000)); } - // console.log('withdrawing', trnsferValue * 6); const rec = await nf3Users[0].withdraw( false, erc20Address, @@ -547,7 +563,6 @@ describe('ERC20 tests', () => { nf3Users[0].ethereumAddress, fee, ); - await new Promise(resolve => setTimeout(resolve, 15000)); expectTransaction(rec); diff --git a/wallet/src/hooks/User/index.jsx b/wallet/src/hooks/User/index.jsx index 887e4a7c5..4f18b6671 100644 --- a/wallet/src/hooks/User/index.jsx +++ b/wallet/src/hooks/User/index.jsx @@ -143,8 +143,8 @@ export const UserProvider = ({ children }) => { useInterval( async () => { const circuitName = USE_STUBS - ? ['deposit_stub', 'single_transfer_stub', 'double_transfer_stub', 'withdraw_stub'] - : ['deposit', 'single_transfer', 'double_transfer', 'withdraw']; + ? ['deposit_stub', 'transfer_stub', 'withdraw_stub'] + : ['deposit', 'transfer', 'withdraw']; const circuitCheck = await Promise.all(circuitName.map(c => checkIndexDBForCircuit(c))); console.log('Circuit Check', circuitCheck); diff --git a/zokrates-worker/src/index.mjs b/zokrates-worker/src/index.mjs index 2850e40a8..12a244e5f 100644 --- a/zokrates-worker/src/index.mjs +++ b/zokrates-worker/src/index.mjs @@ -33,7 +33,7 @@ const checkCircuitsOutput = async () => { : `${DEFAULT_CIRCUIT_FILES_URL}/${env}`; const url = `${baseUrl}/proving_files/hash.txt`; const outputPath = `./output`; - const circuits = ['deposit', 'double_transfer', 'single_transfer', 'withdraw']; + const circuits = ['deposit', 'transfer', 'withdraw']; const res = await axios.get(url); // get all circuit files const files = res.data.split('\n'); diff --git a/zokrates-worker/src/zokrates-lib/compile.mjs b/zokrates-worker/src/zokrates-lib/compile.mjs index 6d9f3b31e..bafd6bb88 100644 --- a/zokrates-worker/src/zokrates-lib/compile.mjs +++ b/zokrates-worker/src/zokrates-lib/compile.mjs @@ -38,7 +38,7 @@ export default async function compile( '-o', `${parsedOutputPath}${parsedOutputName}`, '-s', - `${parsedOutputPath}abi.json`, + `${parsedOutputPath}_abi.json`, '--curve', curve, ],