From 7602dae8f22110f297cdee9f93aa383ecb5efa7d Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Thu, 26 Sep 2024 12:14:18 +0100 Subject: [PATCH] batch test for contracts Signed-off-by: Chengxuan Xing --- ...to_anon_enc_nullifier_kyc_cost_analysis.ts | 28 +- solidity/test/utils.ts | 104 +++++--- solidity/test/zeto_anon.ts | 105 ++++++-- solidity/test/zeto_anon_enc.ts | 121 +++++++-- solidity/test/zeto_anon_enc_nullifier.ts | 171 ++++++++++--- solidity/test/zeto_anon_enc_nullifier_kyc.ts | 204 ++++++++++++--- ...zeto_anon_enc_nullifier_non_repudiation.ts | 239 ++++++++++++++---- solidity/test/zeto_anon_nullifier.ts | 153 ++++++++--- solidity/test/zeto_anon_nullifier_kyc.ts | 173 ++++++++++--- 9 files changed, 1016 insertions(+), 282 deletions(-) diff --git a/solidity/test/gas_cost/zeto_anon_enc_nullifier_kyc_cost_analysis.ts b/solidity/test/gas_cost/zeto_anon_enc_nullifier_kyc_cost_analysis.ts index 0115e4b..db70b54 100644 --- a/solidity/test/gas_cost/zeto_anon_enc_nullifier_kyc_cost_analysis.ts +++ b/solidity/test/gas_cost/zeto_anon_enc_nullifier_kyc_cost_analysis.ts @@ -413,8 +413,8 @@ describe.skip('(Gas cost analysis) Zeto based fungible token with anonymity usin owners: User[], gasHistories: number[] ) { - let nullifiers: [BigNumberish, BigNumberish]; - let outputCommitments: [BigNumberish, BigNumberish]; + let nullifiers: BigNumberish[]; + let outputCommitments: BigNumberish[]; let encryptedValues: BigNumberish[]; let encryptionNonce: BigNumberish; let encodedProof: any; @@ -470,22 +470,18 @@ describe.skip('(Gas cost analysis) Zeto based fungible token with anonymity usin BigNumberish, BigNumberish ]; - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map( + const inputCommitments: BigNumberish[] = inputs.map( (input) => input.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); - const outputCommitments: [BigNumberish, BigNumberish] = outputs.map( + const outputCommitments: BigNumberish[] = outputs.map( (output) => output.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const outputValues = outputs.map((output) => BigInt(output.value || 0n)); - const outputOwnerPublicKeys: [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ] = owners.map((owner) => owner.babyJubPublicKey) as [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ]; + const outputOwnerPublicKeys: BigNumberish[][] = owners.map( + (owner) => owner.babyJubPublicKey + ) as BigNumberish[][]; const encryptionNonce: BigNumberish = newEncryptionNonce() as BigNumberish; const encryptInputs = stringifyBigInts({ encryptionNonce, @@ -499,7 +495,7 @@ describe.skip('(Gas cost analysis) Zeto based fungible token with anonymity usin inputSalts, inputOwnerPrivateKey: signer.formattedPrivateKey, utxosRoot, - enabled: [nullifiers[0] !== 0n ? 1 : 0, nullifiers[1] !== 0n ? 1 : 0], + enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)), utxosMerkleProof, identitiesRoot, identitiesMerkleProof, @@ -539,8 +535,8 @@ describe.skip('(Gas cost analysis) Zeto based fungible token with anonymity usin async function sendTx( signer: User, - nullifiers: [BigNumberish, BigNumberish], - outputCommitments: [BigNumberish, BigNumberish], + nullifiers: BigNumberish[], + outputCommitments: BigNumberish[], root: BigNumberish, encryptedValues: BigNumberish[], encryptionNonce: BigNumberish, diff --git a/solidity/test/utils.ts b/solidity/test/utils.ts index b969788..b932135 100644 --- a/solidity/test/utils.ts +++ b/solidity/test/utils.ts @@ -14,17 +14,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { readFileSync } from "fs"; -import * as path from "path"; +import { readFileSync } from 'fs'; +import * as path from 'path'; import { BigNumberish } from 'ethers'; import { groth16 } from 'snarkjs'; -import { loadCircuit, encodeProof } from "zeto-js"; -import { User, UTXO } from "./lib/utils"; +import { loadCircuit, encodeProof } from 'zeto-js'; +import { User, UTXO } from './lib/utils'; function provingKeysRoot() { const PROVING_KEYS_ROOT = process.env.PROVING_KEYS_ROOT; if (!PROVING_KEYS_ROOT) { - throw new Error("PROVING_KEYS_ROOT env var is not set"); + throw new Error('PROVING_KEYS_ROOT env var is not set'); } return PROVING_KEYS_ROOT; } @@ -45,46 +45,64 @@ export function loadProvingKeys(type: string) { export async function prepareDepositProof(signer: User, output: UTXO) { const outputCommitments: [BigNumberish] = [output.hash] as [BigNumberish]; const outputValues = [BigInt(output.value || 0n)]; - const outputOwnerPublicKeys: [[BigNumberish, BigNumberish]] = [signer.babyJubPublicKey] as [[BigNumberish, BigNumberish]]; + const outputOwnerPublicKeys: [[BigNumberish, BigNumberish]] = [ + signer.babyJubPublicKey, + ] as [[BigNumberish, BigNumberish]]; const inputObj = { outputCommitments, outputValues, outputSalts: [output.salt], - outputOwnerPublicKeys + outputOwnerPublicKeys, }; const circuit = await loadCircuit('check_hashes_value'); const { provingKeyFile } = loadProvingKeys('check_hashes_value'); const startWitnessCalculation = Date.now(); - const witness = await circuit.calculateWTNSBin( - inputObj, - true - ); + const witness = await circuit.calculateWTNSBin(inputObj, true); const timeWithnessCalculation = Date.now() - startWitnessCalculation; const startProofGeneration = Date.now(); - const { proof, publicSignals } = await groth16.prove(provingKeyFile, witness) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; + const { proof, publicSignals } = (await groth16.prove( + provingKeyFile, + witness + )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; const timeProofGeneration = Date.now() - startProofGeneration; - console.log(`Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.`); + console.log( + `Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.` + ); const encodedProof = encodeProof(proof); return { outputCommitments, - encodedProof + encodedProof, }; } -export async function prepareNullifierWithdrawProof(signer: User, inputs: UTXO[], _nullifiers: UTXO[], output: UTXO, root: BigInt, merkleProof: BigInt[][]) { - const nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [BigNumberish, BigNumberish]; - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map((input) => input.hash) as [BigNumberish, BigNumberish]; +export async function prepareNullifierWithdrawProof( + signer: User, + inputs: UTXO[], + _nullifiers: UTXO[], + output: UTXO, + root: BigInt, + merkleProof: BigInt[][] +) { + const nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [ + BigNumberish, + BigNumberish + ]; + const inputCommitments: BigNumberish[] = inputs.map( + (input) => input.hash + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); const outputCommitments: [BigNumberish] = [output.hash] as [BigNumberish]; const outputValues = [BigInt(output.value || 0n)]; - const outputOwnerPublicKeys: [[BigNumberish, BigNumberish]] = [signer.babyJubPublicKey] as [[BigNumberish, BigNumberish]]; + const outputOwnerPublicKeys: [[BigNumberish, BigNumberish]] = [ + signer.babyJubPublicKey, + ] as [[BigNumberish, BigNumberish]]; const inputObj = { nullifiers, @@ -98,39 +116,49 @@ export async function prepareNullifierWithdrawProof(signer: User, inputs: UTXO[] outputCommitments, outputValues, outputSalts: [output.salt], - outputOwnerPublicKeys + outputOwnerPublicKeys, }; const circuit = await loadCircuit('check_nullifier_value'); const { provingKeyFile } = loadProvingKeys('check_nullifier_value'); const startWitnessCalculation = Date.now(); - const witness = await circuit.calculateWTNSBin( - inputObj, - true - ); + const witness = await circuit.calculateWTNSBin(inputObj, true); const timeWithnessCalculation = Date.now() - startWitnessCalculation; const startProofGeneration = Date.now(); - const { proof, publicSignals } = await groth16.prove(provingKeyFile, witness) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; + const { proof, publicSignals } = (await groth16.prove( + provingKeyFile, + witness + )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; const timeProofGeneration = Date.now() - startProofGeneration; - console.log(`Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.`); + console.log( + `Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.` + ); const encodedProof = encodeProof(proof); return { nullifiers, outputCommitments, - encodedProof + encodedProof, }; } -export async function prepareWithdrawProof(signer: User, inputs: UTXO[], output: UTXO) { - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map((input) => input.hash) as [BigNumberish, BigNumberish]; +export async function prepareWithdrawProof( + signer: User, + inputs: UTXO[], + output: UTXO +) { + const inputCommitments: BigNumberish[] = inputs.map( + (input) => input.hash + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); const outputCommitments: [BigNumberish] = [output.hash] as [BigNumberish]; const outputValues = [BigInt(output.value || 0n)]; - const outputOwnerPublicKeys: [[BigNumberish, BigNumberish]] = [signer.babyJubPublicKey] as [[BigNumberish, BigNumberish]]; + const outputOwnerPublicKeys: [[BigNumberish, BigNumberish]] = [ + signer.babyJubPublicKey, + ] as [[BigNumberish, BigNumberish]]; const inputObj = { inputCommitments, @@ -140,28 +168,30 @@ export async function prepareWithdrawProof(signer: User, inputs: UTXO[], output: outputCommitments, outputValues, outputSalts: [output.salt], - outputOwnerPublicKeys + outputOwnerPublicKeys, }; const circuit = await loadCircuit('check_inputs_outputs_value'); const { provingKeyFile } = loadProvingKeys('check_inputs_outputs_value'); const startWitnessCalculation = Date.now(); - const witness = await circuit.calculateWTNSBin( - inputObj, - true - ); + const witness = await circuit.calculateWTNSBin(inputObj, true); const timeWithnessCalculation = Date.now() - startWitnessCalculation; const startProofGeneration = Date.now(); - const { proof, publicSignals } = await groth16.prove(provingKeyFile, witness) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; + const { proof, publicSignals } = (await groth16.prove( + provingKeyFile, + witness + )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; const timeProofGeneration = Date.now() - startProofGeneration; - console.log(`Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.`); + console.log( + `Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.` + ); const encodedProof = encodeProof(proof); return { inputCommitments, outputCommitments, - encodedProof + encodedProof, }; } diff --git a/solidity/test/zeto_anon.ts b/solidity/test/zeto_anon.ts index fd9ed68..2a748ac 100644 --- a/solidity/test/zeto_anon.ts +++ b/solidity/test/zeto_anon.ts @@ -54,6 +54,7 @@ describe('Zeto based fungible token with anonymity without encryption or nullifi let utxo4: UTXO; let utxo7: UTXO; let circuit: any, provingKey: any; + let batchCircuit: any, batchProvingKey: any; before(async function () { if (network.name !== 'hardhat') { @@ -70,6 +71,61 @@ describe('Zeto based fungible token with anonymity without encryption or nullifi circuit = await loadCircuit('anon'); ({ provingKeyFile: provingKey } = loadProvingKeys('anon')); + + batchCircuit = await loadCircuit('anon_batch'); + ({ provingKeyFile: batchProvingKey } = loadProvingKeys('anon_batch')); + }); + + it('(batch) mint to Alice and batch transfer 10 UTXOs honestly to Bob should succeed', async function () { + // first mint the tokens for batch testing + const inputUtxos = []; + for (let i = 0; i < 10; i++) { + // mint 10 utxos + inputUtxos.push(newUTXO(1, Alice)); + } + await doMint(zeto, deployer, inputUtxos); + + // Alice proposes the output UTXOs, 1 utxo to bob, 2 utxos to alice + const _bOut1 = newUTXO(8, Bob); + const _bOut2 = newUTXO(1, Alice); + const _bOut3 = newUTXO(1, Alice); + const outputUtxos = [_bOut1, _bOut2, _bOut3]; + const outputOwners = [Bob, Alice, Alice]; + const inflatedOutputUtxos = [...outputUtxos]; + const inflatedOutputOwners = [...outputOwners]; + for (let i = 0; i < 10 - outputUtxos.length; i++) { + inflatedOutputUtxos.push(ZERO_UTXO); + inflatedOutputOwners.push(Bob); + } + + // Alice transfers UTXOs to Bob + const result = await doTransfer( + Alice, + inputUtxos, + inflatedOutputUtxos, + inflatedOutputOwners + ); + + const events = parseUTXOEvents(zeto, result); + const incomingUTXOs: any = events[0].outputs; + // check the non-empty output hashes are correct + for (let i = 0; i < outputUtxos.length; i++) { + // Bob uses the information received from Alice to reconstruct the UTXO sent to him + const receivedValue = outputUtxos[i].value; + const receivedSalt = outputUtxos[i].salt; + const hash = poseidonHash([ + BigInt(receivedValue), + receivedSalt, + outputOwners[i].babyJubPublicKey[0], + outputOwners[i].babyJubPublicKey[1], + ]); + expect(incomingUTXOs[i]).to.equal(hash); + } + + // check empty hashes are empty + for (let i = outputUtxos.length; i < 10; i++) { + expect(incomingUTXOs[i]).to.equal(0); + } }); it('mint ERC20 tokens to Alice to deposit to Zeto should succeed', async function () { @@ -257,13 +313,19 @@ describe('Zeto based fungible token with anonymity without encryption or nullifi outputs: UTXO[], owners: User[] ) { - let inputCommitments: [BigNumberish, BigNumberish]; - let outputCommitments: [BigNumberish, BigNumberish]; - let outputOwnerAddresses: [AddressLike, AddressLike]; + let inputCommitments: BigNumberish[]; + let outputCommitments: BigNumberish[]; + let outputOwnerAddresses: AddressLike[]; let encodedProof: any; + let circuitToUse = circuit; + let provingKeyToUse = provingKey; + if (inputs.length > 2 || outputs.length > 2) { + circuitToUse = batchCircuit; + provingKeyToUse = batchProvingKey; + } const result = await prepareProof( - circuit, - provingKey, + circuitToUse, + provingKeyToUse, signer, inputs, outputs, @@ -287,15 +349,18 @@ describe('Zeto based fungible token with anonymity without encryption or nullifi async function sendTx( signer: User, - inputCommitments: [BigNumberish, BigNumberish], - outputCommitments: [BigNumberish, BigNumberish], - outputOwnerAddresses: [AddressLike, AddressLike], + inputCommitments: BigNumberish[], + outputCommitments: BigNumberish[], + outputOwnerAddresses: AddressLike[], encodedProof: any ) { const signerAddress = await signer.signer.getAddress(); - const tx = await zeto - .connect(signer.signer) - .transfer(inputCommitments, outputCommitments, encodedProof, '0x'); + const tx = await zeto.connect(signer.signer).transfer( + inputCommitments.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works + outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works + encodedProof, + '0x' + ); const results = await tx.wait(); console.log(`Method transfer() complete. Gas used: ${results?.gasUsed}`); @@ -319,23 +384,19 @@ async function prepareProof( outputs: UTXO[], owners: User[] ) { - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map( + const inputCommitments: BigNumberish[] = inputs.map( (input) => input.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); - const outputCommitments: [BigNumberish, BigNumberish] = outputs.map( + const outputCommitments: BigNumberish[] = outputs.map( (output) => output.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const outputValues = outputs.map((output) => BigInt(output.value || 0n)); const outputSalts = outputs.map((o) => o.salt || 0n); - const outputOwnerPublicKeys: [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ] = owners.map((owner) => owner.babyJubPublicKey || ZERO_PUBKEY) as [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ]; + const outputOwnerPublicKeys: BigNumberish[][] = owners.map( + (owner) => owner.babyJubPublicKey || ZERO_PUBKEY + ) as BigNumberish[][]; const otherInputs = stringifyBigInts({ inputOwnerPrivateKey: formatPrivKeyForBabyJub(signer.babyJubPrivateKey), }); diff --git a/solidity/test/zeto_anon_enc.ts b/solidity/test/zeto_anon_enc.ts index 44dcd77..0ecf9b2 100644 --- a/solidity/test/zeto_anon_enc.ts +++ b/solidity/test/zeto_anon_enc.ts @@ -61,6 +61,7 @@ describe('Zeto based fungible token with anonymity and encryption', function () let utxo3: UTXO; let utxo4: UTXO; let circuit: any, provingKey: any; + let batchCircuit: any, batchProvingKey: any; before(async function () { if (network.name !== 'hardhat') { @@ -77,6 +78,72 @@ describe('Zeto based fungible token with anonymity and encryption', function () circuit = await loadCircuit('anon_enc'); ({ provingKeyFile: provingKey } = loadProvingKeys('anon_enc')); + batchCircuit = await loadCircuit('anon_enc_batch'); + ({ provingKeyFile: batchProvingKey } = loadProvingKeys('anon_enc_batch')); + }); + + it('(batch) mint to Alice and batch transfer 10 UTXOs honestly to Bob should succeed', async function () { + // first mint the tokens for batch testing + const inputUtxos = []; + for (let i = 0; i < 10; i++) { + // mint 10 utxos + inputUtxos.push(newUTXO(1, Alice)); + } + await doMint(zeto, deployer, inputUtxos); + + // Alice proposes the output UTXOs, 1 utxo to bob, 2 utxos to alice + const _bOut1 = newUTXO(8, Bob); + const _bOut2 = newUTXO(1, Alice); + const _bOut3 = newUTXO(1, Alice); + const outputUtxos = [_bOut1, _bOut2, _bOut3]; + const outputOwners = [Bob, Alice, Alice]; + const inflatedOutputUtxos = [...outputUtxos]; + const inflatedOutputOwners = [...outputOwners]; + for (let i = 0; i < 10 - outputUtxos.length; i++) { + inflatedOutputUtxos.push(ZERO_UTXO); + inflatedOutputOwners.push(Bob); + } + + // Alice transfers UTXOs to Bob + const result = await doTransfer( + Alice, + inputUtxos, + inflatedOutputUtxos, + inflatedOutputOwners + ); + + const events = parseUTXOEvents(zeto, result.txResult!); + expect(events[0].inputs).to.deep.equal(inputUtxos.map((i) => i.hash)); + const incomingUTXOs: any = events[0].outputs; + // Bob reconstructs the shared key using his private key and Alice's public key (obtained out of band) + const senderPublicKey = Alice.babyJubPublicKey; + + const sharedKey = genEcdhSharedKey(Bob.babyJubPrivateKey, senderPublicKey); + const plainText = poseidonDecrypt( + events[0].encryptedValues, + sharedKey, + events[0].encryptionNonce, + 2 + ); + expect(plainText).to.deep.equal([8n, result.plainTextSalt]); + // only the first utxo can be decrypted + const hash = poseidonHash([ + BigInt(plainText[0]), + plainText[1], + Bob.babyJubPublicKey[0], + Bob.babyJubPublicKey[1], + ]); + expect(hash).to.equal(incomingUTXOs[0]); + + // check the non-empty output hashes are correct + for (let i = 1; i < outputUtxos.length; i++) { + expect(incomingUTXOs[i]).to.equal(outputUtxos[i].hash); + } + + // check empty hashes are empty + for (let i = outputUtxos.length; i < 10; i++) { + expect(incomingUTXOs[i]).to.equal(0); + } }); it('mint ERC20 tokens to Alice to deposit to Zeto should succeed', async function () { @@ -268,8 +335,8 @@ describe('Zeto based fungible token with anonymity and encryption', function () outputs: UTXO[], owners: User[] ) { - let inputCommitments: [BigNumberish, BigNumberish]; - let outputCommitments: [BigNumberish, BigNumberish]; + let inputCommitments: BigNumberish[]; + let outputCommitments: BigNumberish[]; let encryptedValues: BigNumberish[]; let encryptionNonce: BigNumberish; let encodedProof: any; @@ -298,30 +365,32 @@ describe('Zeto based fungible token with anonymity and encryption', function () outputs: UTXO[], owners: User[] ) { - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map( + const inputCommitments: BigNumberish[] = inputs.map( (input) => input.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); - const outputCommitments: [BigNumberish, BigNumberish] = outputs.map( + const outputCommitments: BigNumberish[] = outputs.map( (output) => output.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const outputValues = outputs.map((output) => BigInt(output.value || 0n)); - const outputOwnerPublicKeys: [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ] = owners.map((owner) => owner.babyJubPublicKey) as [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ]; + const outputOwnerPublicKeys: BigNumberish[][] = owners.map( + (owner) => owner.babyJubPublicKey + ) as BigNumberish[][]; const encryptionNonce: BigNumberish = newEncryptionNonce() as BigNumberish; const encryptInputs = stringifyBigInts({ encryptionNonce, inputOwnerPrivateKey: formatPrivKeyForBabyJub(signer.babyJubPrivateKey), }); + let circuitToUse = circuit; + let provingKeyToUse = provingKey; + if (inputCommitments.length > 2 || outputCommitments.length > 2) { + circuitToUse = batchCircuit; + provingKeyToUse = batchProvingKey; + } const startWitnessCalculation = Date.now(); - const witness = await circuit.calculateWTNSBin( + const witness = await circuitToUse.calculateWTNSBin( { inputCommitments, inputValues, @@ -338,7 +407,7 @@ describe('Zeto based fungible token with anonymity and encryption', function () const startProofGeneration = Date.now(); const { proof, publicSignals } = (await groth16.prove( - provingKey, + provingKeyToUse, witness )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; const timeProofGeneration = Date.now() - startProofGeneration; @@ -359,22 +428,20 @@ describe('Zeto based fungible token with anonymity and encryption', function () async function sendTx( signer: User, - inputCommitments: [BigNumberish, BigNumberish], - outputCommitments: [BigNumberish, BigNumberish], + inputCommitments: BigNumberish[], + outputCommitments: BigNumberish[], encryptedValues: BigNumberish[], encryptionNonce: BigNumberish, encodedProof: any ) { - const tx = await zeto - .connect(signer.signer) - .transfer( - inputCommitments, - outputCommitments, - encryptionNonce, - encryptedValues, - encodedProof, - '0x' - ); + const tx = await zeto.connect(signer.signer).transfer( + inputCommitments.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works + outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works + encryptionNonce, + encryptedValues, + encodedProof, + '0x' + ); const results: ContractTransactionReceipt | null = await tx.wait(); for (const input of inputCommitments) { diff --git a/solidity/test/zeto_anon_enc_nullifier.ts b/solidity/test/zeto_anon_enc_nullifier.ts index 1663082..fbe155a 100644 --- a/solidity/test/zeto_anon_enc_nullifier.ts +++ b/solidity/test/zeto_anon_enc_nullifier.ts @@ -21,6 +21,7 @@ import { loadCircuit, poseidonDecrypt, encodeProof, + Poseidon, newEncryptionNonce, } from 'zeto-js'; import { groth16 } from 'snarkjs'; @@ -42,6 +43,7 @@ import { prepareNullifierWithdrawProof, } from './utils'; import { deployZeto } from './lib/deploy'; +const poseidonHash = Poseidon.poseidon4; describe('Zeto based fungible token with anonymity using nullifiers and encryption', function () { let deployer: Signer; @@ -57,6 +59,7 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti let utxo4: UTXO; let utxo7: UTXO; let circuit: any, provingKey: any; + let batchCircuit: any, batchProvingKey: any; let smtAlice: Merkletree; let smtBob: Merkletree; @@ -73,14 +76,18 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti ({ deployer, zeto, erc20 } = await deployZeto('Zeto_AnonEncNullifier')); - circuit = await loadCircuit('anon_enc_nullifier'); - ({ provingKeyFile: provingKey } = loadProvingKeys('anon_enc_nullifier')); - const storage1 = new InMemoryDB(str2Bytes('')); smtAlice = new Merkletree(storage1, true, 64); const storage2 = new InMemoryDB(str2Bytes('')); smtBob = new Merkletree(storage2, true, 64); + + circuit = await loadCircuit('anon_enc_nullifier'); + ({ provingKeyFile: provingKey } = loadProvingKeys('anon_enc_nullifier')); + batchCircuit = await loadCircuit('anon_enc_nullifier_batch'); + ({ provingKeyFile: batchProvingKey } = loadProvingKeys( + 'anon_enc_nullifier_batch' + )); }); it('onchain SMT root should be equal to the offchain SMT root', async function () { @@ -90,6 +97,103 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti expect(root.string()).to.equal(onchainRoot.toString()); }); + it('(batch) mint to Alice and batch transfer 10 UTXOs honestly to Bob should succeed', async function () { + // first mint the tokens for batch testing + const inputUtxos = []; + const nullifiers = []; + for (let i = 0; i < 10; i++) { + // mint 10 utxos + const _utxo = newUTXO(1, Alice); + nullifiers.push(newNullifier(_utxo, Alice)); + inputUtxos.push(_utxo); + } + const mintResult = await doMint(zeto, deployer, inputUtxos); + + const mintEvents = parseUTXOEvents(zeto, mintResult); + const mintedHashes = mintEvents[0].outputs; + for (let i = 0; i < mintedHashes.length; i++) { + if (mintedHashes[i] !== 0) { + await smtAlice.add(mintedHashes[i], mintedHashes[i]); + await smtBob.add(mintedHashes[i], mintedHashes[i]); + } + } + // Alice generates inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const mtps = []; + for (let i = 0; i < inputUtxos.length; i++) { + const p = await smtAlice.generateCircomVerifierProof( + inputUtxos[i].hash, + root + ); + mtps.push(p.siblings.map((s) => s.bigInt())); + } + + // Alice proposes the output UTXOs, 1 utxo to bob, 2 utxos to alice + const _bOut1 = newUTXO(8, Bob); + const _bOut2 = newUTXO(1, Alice); + const _bOut3 = newUTXO(1, Alice); + const outputUtxos = [_bOut1, _bOut2, _bOut3]; + const outputOwners = [Bob, Alice, Alice]; + const inflatedOutputUtxos = [...outputUtxos]; + const inflatedOutputOwners = [...outputOwners]; + for (let i = 0; i < 10 - outputUtxos.length; i++) { + inflatedOutputUtxos.push(ZERO_UTXO); + inflatedOutputOwners.push(Bob); + } + // Alice transfers her UTXOs to Bob + const result = await doTransfer( + Alice, + inputUtxos, + nullifiers, + inflatedOutputUtxos, + root.bigInt(), + mtps, + inflatedOutputOwners + ); + + const signerAddress = await Alice.signer.getAddress(); + const events = parseUTXOEvents(zeto, result.txResult!); + expect(events[0].submitter).to.equal(signerAddress); + expect(events[0].inputs).to.deep.equal(nullifiers.map((n) => n.hash)); + + const incomingUTXOs: any = events[0].outputs; + + // Bob uses the encrypted values in the event to decrypt and recover the UTXO value and salt + const sharedKey = genEcdhSharedKey( + Bob.babyJubPrivateKey, + Alice.babyJubPublicKey + ); + const plainText = poseidonDecrypt( + events[0].encryptedValues, + sharedKey, + events[0].encryptionNonce, + 2 + ); + expect(plainText).to.deep.equal([8n, result.plainTextSalt]); + // only the first utxo can be decrypted + const hash = poseidonHash([ + BigInt(plainText[0]), + plainText[1], + Bob.babyJubPublicKey[0], + Bob.babyJubPublicKey[1], + ]); + expect(hash).to.equal(incomingUTXOs[0]); + await smtAlice.add(incomingUTXOs[0], incomingUTXOs[0]); + await smtBob.add(incomingUTXOs[0], incomingUTXOs[0]); + + // check the non-empty output hashes are correct + for (let i = 1; i < outputUtxos.length; i++) { + expect(incomingUTXOs[i]).to.equal(outputUtxos[i].hash); + await smtAlice.add(incomingUTXOs[i], incomingUTXOs[i]); + await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]); + } + + // check empty hashes are empty + for (let i = outputUtxos.length; i < 10; i++) { + expect(incomingUTXOs[i]).to.equal(0); + } + }); + it('mint ERC20 tokens to Alice to deposit to Zeto should succeed', async function () { const startingBalance = await erc20.balanceOf(Alice.ethAddress); const tx = await erc20.connect(deployer).mint(Alice.ethAddress, 100); @@ -514,8 +618,8 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti merkleProofs: BigInt[][], owners: User[] ) { - let nullifiers: [BigNumberish, BigNumberish]; - let outputCommitments: [BigNumberish, BigNumberish]; + let nullifiers: BigNumberish[]; + let outputCommitments: BigNumberish[]; let encryptedValues: BigNumberish[]; let encryptionNonce: BigNumberish; let encodedProof: any; @@ -563,27 +667,28 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti BigNumberish, BigNumberish ]; - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map( + const inputCommitments: BigNumberish[] = inputs.map( (input) => input.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); - const outputCommitments: [BigNumberish, BigNumberish] = outputs.map( + const outputCommitments: BigNumberish[] = outputs.map( (output) => output.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const outputValues = outputs.map((output) => BigInt(output.value || 0n)); - const outputOwnerPublicKeys: [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ] = owners.map((owner) => owner.babyJubPublicKey) as [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ]; + const outputOwnerPublicKeys: BigNumberish[][] = owners.map( + (owner) => owner.babyJubPublicKey + ) as BigNumberish[][]; const encryptionNonce: BigNumberish = newEncryptionNonce() as BigNumberish; const encryptInputs = stringifyBigInts({ encryptionNonce, }); - + let circuitToUse = circuit; + let provingKeyToUse = provingKey; + if (inputCommitments.length > 2 || outputCommitments.length > 2) { + circuitToUse = batchCircuit; + provingKeyToUse = batchProvingKey; + } const startWitnessCalculation = Date.now(); const inputObj = { nullifiers, @@ -592,20 +697,20 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti inputSalts, inputOwnerPrivateKey: signer.formattedPrivateKey, root, - enabled: [nullifiers[0] !== 0n ? 1 : 0, nullifiers[1] !== 0n ? 1 : 0], + enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)), merkleProof, outputCommitments, outputValues, - outputSalts: outputs.map((output) => output.salt), + outputSalts: outputs.map((output) => output.salt || 0n), outputOwnerPublicKeys, ...encryptInputs, }; - const witness = await circuit.calculateWTNSBin(inputObj, true); + const witness = await circuitToUse.calculateWTNSBin(inputObj, true); const timeWithnessCalculation = Date.now() - startWitnessCalculation; const startProofGeneration = Date.now(); const { proof, publicSignals } = (await groth16.prove( - provingKey, + provingKeyToUse, witness )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; const timeProofGeneration = Date.now() - startProofGeneration; @@ -627,25 +732,23 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti async function sendTx( signer: User, - nullifiers: [BigNumberish, BigNumberish], - outputCommitments: [BigNumberish, BigNumberish], + nullifiers: BigNumberish[], + outputCommitments: BigNumberish[], root: BigNumberish, encryptedValues: BigNumberish[], encryptionNonce: BigNumberish, encodedProof: any ) { const startTx = Date.now(); - const tx = await zeto - .connect(signer.signer) - .transfer( - nullifiers, - outputCommitments, - root, - encryptionNonce, - encryptedValues, - encodedProof, - '0x' - ); + const tx = await zeto.connect(signer.signer).transfer( + nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works + outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works + root, + encryptionNonce, + encryptedValues, + encodedProof, + '0x' + ); const results: ContractTransactionReceipt | null = await tx.wait(); console.log( `Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${ diff --git a/solidity/test/zeto_anon_enc_nullifier_kyc.ts b/solidity/test/zeto_anon_enc_nullifier_kyc.ts index ffacde0..51baa40 100644 --- a/solidity/test/zeto_anon_enc_nullifier_kyc.ts +++ b/solidity/test/zeto_anon_enc_nullifier_kyc.ts @@ -21,6 +21,7 @@ import { loadCircuit, poseidonDecrypt, encodeProof, + Poseidon, newEncryptionNonce, kycHash, } from 'zeto-js'; @@ -44,6 +45,7 @@ import { prepareNullifierWithdrawProof, } from './utils'; import { deployZeto } from './lib/deploy'; +const poseidonHash = Poseidon.poseidon4; describe('Zeto based fungible token with anonymity using nullifiers and encryption with KYC', function () { let deployer: Signer; @@ -63,9 +65,11 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti let utxo7: UTXO; let withdrawChangeUTXO: UTXO; let circuit: any, provingKey: any; + let batchCircuit: any, batchProvingKey: any; let smtAlice: Merkletree; let smtBob: Merkletree; let smtKyc: Merkletree; + let smtUnregistered: Merkletree; before(async function () { if (network.name !== 'hardhat') { @@ -88,11 +92,6 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti const tx4 = await zeto.connect(deployer).register(Charlie.babyJubPublicKey); const result3 = await tx4.wait(); - circuit = await loadCircuit('anon_enc_nullifier_kyc'); - ({ provingKeyFile: provingKey } = loadProvingKeys( - 'anon_enc_nullifier_kyc' - )); - const storage1 = new InMemoryDB(str2Bytes('alice')); smtAlice = new Merkletree(storage1, true, 64); @@ -102,12 +101,24 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti const storage3 = new InMemoryDB(str2Bytes('kyc')); smtKyc = new Merkletree(storage3, true, 10); + const storage4 = new InMemoryDB(str2Bytes('unregistered')); + smtUnregistered = new Merkletree(storage4, true, 64); + const publicKey1 = parseRegistryEvents(zeto, result1); await smtKyc.add(kycHash(publicKey1), kycHash(publicKey1)); const publicKey2 = parseRegistryEvents(zeto, result2); await smtKyc.add(kycHash(publicKey2), kycHash(publicKey2)); const publicKey3 = parseRegistryEvents(zeto, result3); await smtKyc.add(kycHash(publicKey3), kycHash(publicKey3)); + + circuit = await loadCircuit('anon_enc_nullifier_kyc'); + ({ provingKeyFile: provingKey } = loadProvingKeys( + 'anon_enc_nullifier_kyc' + )); + batchCircuit = await loadCircuit('anon_enc_nullifier_kyc_batch'); + ({ provingKeyFile: batchProvingKey } = loadProvingKeys( + 'anon_enc_nullifier_kyc_batch' + )); }); it('onchain SMT root should be equal to the offchain SMT root', async function () { @@ -117,6 +128,123 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti expect(root.string()).to.equal(onchainRoot.toString()); }); + it('(batch) mint to Alice and batch transfer 10 UTXOs honestly to Bob should succeed', async function () { + // first mint the tokens for batch testing + const inputUtxos = []; + const nullifiers = []; + for (let i = 0; i < 10; i++) { + // mint 10 utxos + const _utxo = newUTXO(1, Alice); + nullifiers.push(newNullifier(_utxo, Alice)); + inputUtxos.push(_utxo); + } + const mintResult = await doMint(zeto, deployer, inputUtxos); + + const mintEvents = parseUTXOEvents(zeto, mintResult); + const mintedHashes = mintEvents[0].outputs; + for (let i = 0; i < mintedHashes.length; i++) { + if (mintedHashes[i] !== 0) { + await smtAlice.add(mintedHashes[i], mintedHashes[i]); + await smtBob.add(mintedHashes[i], mintedHashes[i]); + await smtUnregistered.add(mintedHashes[i], mintedHashes[i]); + } + } + // Alice generates inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const mtps = []; + for (let i = 0; i < inputUtxos.length; i++) { + const p = await smtAlice.generateCircomVerifierProof( + inputUtxos[i].hash, + root + ); + mtps.push(p.siblings.map((s) => s.bigInt())); + } + + // Alice generates inclusion proofs for the identities in the transaction + const identitiesRoot = await smtKyc.root(); + const aProof = await smtKyc.generateCircomVerifierProof( + kycHash(Alice.babyJubPublicKey), + identitiesRoot + ); + const aliceProof = aProof.siblings.map((s) => s.bigInt()); + const bProof = await smtKyc.generateCircomVerifierProof( + kycHash(Bob.babyJubPublicKey), + identitiesRoot + ); + const bobProof = bProof.siblings.map((s) => s.bigInt()); + + // Alice proposes the output UTXOs, 1 utxo to bob, 2 utxos to alice + const _bOut1 = newUTXO(8, Bob); + const _bOut2 = newUTXO(1, Alice); + const _bOut3 = newUTXO(1, Alice); + const outputUtxos = [_bOut1, _bOut2, _bOut3]; + const outputOwners = [Bob, Alice, Alice]; + const identityMerkleProofs = [aliceProof, bobProof, aliceProof, aliceProof]; + const inflatedOutputUtxos = [...outputUtxos]; + const inflatedOutputOwners = [...outputOwners]; + for (let i = 0; i < 10 - outputUtxos.length; i++) { + inflatedOutputUtxos.push(ZERO_UTXO); + inflatedOutputOwners.push(Bob); + identityMerkleProofs.push(bobProof); + } + // Alice transfers her UTXOs to Bob + const result = await doTransfer( + Alice, + inputUtxos, + nullifiers, + inflatedOutputUtxos, + root.bigInt(), + mtps, + identitiesRoot.bigInt(), + identityMerkleProofs, + inflatedOutputOwners + ); + + const signerAddress = await Alice.signer.getAddress(); + const events = parseUTXOEvents(zeto, result.txResult!); + expect(events[0].submitter).to.equal(signerAddress); + expect(events[0].inputs).to.deep.equal(nullifiers.map((n) => n.hash)); + + const incomingUTXOs: any = events[0].outputs; + + // Bob uses the encrypted values in the event to decrypt and recover the UTXO value and salt + const sharedKey = genEcdhSharedKey( + Bob.babyJubPrivateKey, + Alice.babyJubPublicKey + ); + const plainText = poseidonDecrypt( + events[0].encryptedValues, + sharedKey, + events[0].encryptionNonce, + 2 + ); + expect(plainText).to.deep.equal([8n, result.plainTextSalt]); + // only the first utxo can be decrypted + const hash = poseidonHash([ + BigInt(plainText[0]), + plainText[1], + Bob.babyJubPublicKey[0], + Bob.babyJubPublicKey[1], + ]); + expect(hash).to.equal(incomingUTXOs[0]); + await smtAlice.add(incomingUTXOs[0], incomingUTXOs[0]); + await smtBob.add(incomingUTXOs[0], incomingUTXOs[0]); + await smtUnregistered.add(incomingUTXOs[0], incomingUTXOs[0]); + + // check the non-empty output hashes are correct + for (let i = 1; i < outputUtxos.length; i++) { + expect(incomingUTXOs[i]).to.equal(outputUtxos[i].hash); + await smtAlice.add(incomingUTXOs[i], incomingUTXOs[i]); + await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]); + await smtUnregistered.add(incomingUTXOs[i], incomingUTXOs[i]); + } + + // check empty hashes are empty + for (let i = outputUtxos.length; i < 10; i++) { + expect(incomingUTXOs[i]).to.equal(0); + } + }); + it('mint ERC20 tokens to Alice to deposit to Zeto should succeed', async function () { const startingBalance = await erc20.balanceOf(Alice.ethAddress); const tx = await erc20.connect(deployer).mint(Alice.ethAddress, 100); @@ -368,15 +496,8 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti }); describe('unregistered user cases', function () { - let storage3; - let smtUnregistered: Merkletree; let unregisteredUtxo100: UTXO; - before(() => { - storage3 = new InMemoryDB(str2Bytes('unregistered')); - smtUnregistered = new Merkletree(storage3, true, 64); - }); - it('deposit by an unregistered user should succeed', async function () { const tx = await erc20 .connect(deployer) @@ -817,8 +938,8 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti identitiesMerkleProof: BigInt[][], owners: User[] ) { - let nullifiers: [BigNumberish, BigNumberish]; - let outputCommitments: [BigNumberish, BigNumberish]; + let nullifiers: BigNumberish[]; + let outputCommitments: BigNumberish[]; let encryptedValues: BigNumberish[]; let encryptionNonce: BigNumberish; let encodedProof: any; @@ -871,27 +992,28 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti BigNumberish, BigNumberish ]; - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map( + const inputCommitments: BigNumberish[] = inputs.map( (input) => input.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); - const outputCommitments: [BigNumberish, BigNumberish] = outputs.map( + const outputCommitments: BigNumberish[] = outputs.map( (output) => output.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const outputValues = outputs.map((output) => BigInt(output.value || 0n)); - const outputOwnerPublicKeys: [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ] = owners.map((owner) => owner.babyJubPublicKey) as [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ]; + const outputOwnerPublicKeys: BigNumberish[][] = owners.map( + (owner) => owner.babyJubPublicKey + ) as BigNumberish[][]; const encryptionNonce: BigNumberish = newEncryptionNonce() as BigNumberish; const encryptInputs = stringifyBigInts({ encryptionNonce, }); - + let circuitToUse = circuit; + let provingKeyToUse = provingKey; + if (inputCommitments.length > 2 || outputCommitments.length > 2) { + circuitToUse = batchCircuit; + provingKeyToUse = batchProvingKey; + } const startWitnessCalculation = Date.now(); const inputObj = { nullifiers, @@ -900,7 +1022,7 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti inputSalts, inputOwnerPrivateKey: signer.formattedPrivateKey, utxosRoot, - enabled: [nullifiers[0] !== 0n ? 1 : 0, nullifiers[1] !== 0n ? 1 : 0], + enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)), utxosMerkleProof, identitiesRoot, identitiesMerkleProof, @@ -910,12 +1032,12 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti outputOwnerPublicKeys, ...encryptInputs, }; - const witness = await circuit.calculateWTNSBin(inputObj, true); + const witness = await circuitToUse.calculateWTNSBin(inputObj, true); const timeWithnessCalculation = Date.now() - startWitnessCalculation; const startProofGeneration = Date.now(); const { proof, publicSignals } = (await groth16.prove( - provingKey, + provingKeyToUse, witness )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; const timeProofGeneration = Date.now() - startProofGeneration; @@ -937,25 +1059,23 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti async function sendTx( signer: User, - nullifiers: [BigNumberish, BigNumberish], - outputCommitments: [BigNumberish, BigNumberish], + nullifiers: BigNumberish[], + outputCommitments: BigNumberish[], root: BigNumberish, encryptedValues: BigNumberish[], encryptionNonce: BigNumberish, encodedProof: any ) { const startTx = Date.now(); - const tx = await zeto - .connect(signer.signer) - .transfer( - nullifiers, - outputCommitments, - root, - encryptionNonce, - encryptedValues, - encodedProof, - '0x' - ); + const tx = await zeto.connect(signer.signer).transfer( + nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works + outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works + root, + encryptionNonce, + encryptedValues, + encodedProof, + '0x' + ); const results: ContractTransactionReceipt | null = await tx.wait(); console.log( `Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${ diff --git a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts index dd7ecc2..d5f47a6 100644 --- a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts +++ b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts @@ -43,6 +43,7 @@ import { prepareNullifierWithdrawProof, } from './utils'; import { deployZeto } from './lib/deploy'; +const poseidonHash = Poseidon.poseidon4; describe('Zeto based fungible token with anonymity using nullifiers and encryption for non-repudiation', function () { let deployer: Signer; @@ -59,6 +60,7 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti let utxo4: UTXO; let utxo7: UTXO; let circuit: any, provingKey: any; + let batchCircuit: any, batchProvingKey: any; let smtAlice: Merkletree; let smtBob: Merkletree; @@ -83,16 +85,22 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti .setArbiter(Authority.babyJubPublicKey); await tx1.wait(); - circuit = await loadCircuit('anon_enc_nullifier_non_repudiation'); - ({ provingKeyFile: provingKey } = loadProvingKeys( - 'anon_enc_nullifier_non_repudiation' - )); - const storage1 = new InMemoryDB(str2Bytes('')); smtAlice = new Merkletree(storage1, true, 64); const storage2 = new InMemoryDB(str2Bytes('')); smtBob = new Merkletree(storage2, true, 64); + + circuit = await loadCircuit('anon_enc_nullifier_non_repudiation'); + ({ provingKeyFile: provingKey } = loadProvingKeys( + 'anon_enc_nullifier_non_repudiation' + )); + batchCircuit = await loadCircuit( + 'anon_enc_nullifier_non_repudiation_batch' + ); + ({ provingKeyFile: batchProvingKey } = loadProvingKeys( + 'anon_enc_nullifier_non_repudiation_batch' + )); }); it('onchain SMT root should be equal to the offchain SMT root', async function () { @@ -102,6 +110,148 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti expect(root.string()).to.equal(onchainRoot.toString()); }); + it('(batch) mint to Alice and batch transfer 10 UTXOs honestly to Bob should succeed', async function () { + // first mint the tokens for batch testing + const inputUtxos = []; + const nullifiers = []; + for (let i = 0; i < 10; i++) { + // mint 10 utxos + const _utxo = newUTXO(1, Alice); + nullifiers.push(newNullifier(_utxo, Alice)); + inputUtxos.push(_utxo); + } + const mintResult = await doMint(zeto, deployer, inputUtxos); + + const mintEvents = parseUTXOEvents(zeto, mintResult); + const mintedHashes = mintEvents[0].outputs; + for (let i = 0; i < mintedHashes.length; i++) { + if (mintedHashes[i] !== 0) { + await smtAlice.add(mintedHashes[i], mintedHashes[i]); + await smtBob.add(mintedHashes[i], mintedHashes[i]); + } + } + // Alice generates inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const mtps = []; + for (let i = 0; i < inputUtxos.length; i++) { + const p = await smtAlice.generateCircomVerifierProof( + inputUtxos[i].hash, + root + ); + mtps.push(p.siblings.map((s) => s.bigInt())); + } + + + // Alice proposes the output UTXOs, 1 utxo to bob, 2 utxos to alice + const _bOut1 = newUTXO(8, Bob); + const _bOut2 = newUTXO(1, Alice); + const _bOut3 = newUTXO(1, Alice); + const outputUtxos = [_bOut1, _bOut2, _bOut3]; + const outputOwners = [Bob, Alice, Alice]; + const inflatedOutputUtxos = [...outputUtxos]; + const inflatedOutputOwners = [...outputOwners]; + for (let i = 0; i < 10 - outputUtxos.length; i++) { + inflatedOutputUtxos.push(ZERO_UTXO); + inflatedOutputOwners.push(Bob); + } + // Alice transfers her UTXOs to Bob + const result = await doTransfer( + Alice, + inputUtxos, + nullifiers, + inflatedOutputUtxos, + root.bigInt(), + mtps, + inflatedOutputOwners + ); + + const signerAddress = await Alice.signer.getAddress(); + const events = parseUTXOEvents(zeto, result.txResult!); + expect(events[0].submitter).to.equal(signerAddress); + expect(events[0].inputs).to.deep.equal(nullifiers.map((n) => n.hash)); + + const incomingUTXOs: any = events[0].outputs; + + // Bob uses the encrypted values in the event to decrypt and recover the UTXO value and salt + const sharedKey = genEcdhSharedKey( + Bob.babyJubPrivateKey, + Alice.babyJubPublicKey + ); + + const plainText = poseidonDecrypt( + events[0].encryptedValuesForReceiver, + sharedKey, + events[0].encryptionNonce, + 2 + ); + expect(plainText).to.deep.equal([8n, result.plainTextSalt]); + // only the first utxo can be decrypted + const hash = poseidonHash([ + BigInt(plainText[0]), + plainText[1], + Bob.babyJubPublicKey[0], + Bob.babyJubPublicKey[1], + ]); + expect(hash).to.equal(incomingUTXOs[0]); + await smtAlice.add(incomingUTXOs[0], incomingUTXOs[0]); + await smtBob.add(incomingUTXOs[0], incomingUTXOs[0]); + + // check the non-empty output hashes are correct + for (let i = 1; i < outputUtxos.length; i++) { + expect(incomingUTXOs[i]).to.equal(outputUtxos[i].hash); + await smtAlice.add(incomingUTXOs[i], incomingUTXOs[i]); + await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]); + } + + // check empty hashes are empty + for (let i = outputUtxos.length; i < 10; i++) { + expect(incomingUTXOs[i]).to.equal(0); + } + + // The regulator uses the encrypted values in the event to decrypt and recover the UTXO value and salt + const auditKey = genEcdhSharedKey( + Authority.babyJubPrivateKey, + Alice.babyJubPublicKey + ); + const auditPlainText = poseidonDecrypt( + events[0].encryptedValuesForAuthority, + auditKey, + events[0].encryptionNonce, + 62 + ); + + // check sender pub key match + expect(auditPlainText[0]).to.equal(Alice.babyJubPublicKey[0]); + expect(auditPlainText[1]).to.equal(Alice.babyJubPublicKey[1]); + // check input values salts match + for (let i = 0; i < inputUtxos.length; i++) { + const calHash = poseidonHash([ + auditPlainText[2 * i + 2], + auditPlainText[2 * i + 3], + Alice.babyJubPublicKey[0], + Alice.babyJubPublicKey[1], + ]); + expect(calHash).to.equal(inputUtxos[i].hash); + } + // check output values salts match + // check input values salts match + for (let i = 0; i < outputUtxos.length; i++) { + const calHash = poseidonHash([ + auditPlainText[2 * i + 42], + auditPlainText[2 * i + 43], + auditPlainText[2 * i + 22], + auditPlainText[2 * i + 23], + ]); + expect(calHash).to.equal(outputUtxos[i].hash); + } + + // check empty hashes are empty + for (let i = outputUtxos.length; i < 10; i++) { + expect(auditPlainText[2 * i + 42]).to.equal(0); + expect(auditPlainText[2 * i + 43]).to.equal(0); + } + }); + it('mint ERC20 tokens to Alice to deposit to Zeto should succeed', async function () { const startingBalance = await erc20.balanceOf(Alice.ethAddress); const tx = await erc20.connect(deployer).mint(Alice.ethAddress, 100); @@ -575,8 +725,8 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti merkleProofs: BigInt[][], owners: User[] ) { - let nullifiers: [BigNumberish, BigNumberish]; - let outputCommitments: [BigNumberish, BigNumberish]; + let nullifiers: BigNumberish[]; + let outputCommitments: BigNumberish[]; let encryptedValues: BigNumberish[]; let encryptionNonce: BigNumberish; let encodedProof: any; @@ -595,7 +745,6 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti ]; outputCommitments = result.outputCommitments; encodedProof = result.encodedProof; - encryptedValues = result.encryptedValues; encryptionNonce = result.encryptionNonce; const txResult = await sendTx( @@ -603,7 +752,8 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti nullifiers, outputCommitments, root, - encryptedValues, + result.encryptedValuesForReceiver, + result.encryptedValuesForRegulator, encryptionNonce, encodedProof ); @@ -624,27 +774,30 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti BigNumberish, BigNumberish ]; - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map( + const inputCommitments: BigNumberish[] = inputs.map( (input) => input.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); - const outputCommitments: [BigNumberish, BigNumberish] = outputs.map( + const outputCommitments: BigNumberish[] = outputs.map( (output) => output.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const outputValues = outputs.map((output) => BigInt(output.value || 0n)); - const outputOwnerPublicKeys: [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ] = owners.map((owner) => owner.babyJubPublicKey) as [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ]; + const outputOwnerPublicKeys: BigNumberish[][] = owners.map( + (owner) => owner.babyJubPublicKey + ) as BigNumberish[][]; const encryptionNonce: BigNumberish = newEncryptionNonce() as BigNumberish; const encryptInputs = stringifyBigInts({ encryptionNonce, }); - + let circuitToUse = circuit; + let provingKeyToUse = provingKey; + let isBatch = false; + if (inputCommitments.length > 2 || outputCommitments.length > 2) { + isBatch = true; + circuitToUse = batchCircuit; + provingKeyToUse = batchProvingKey; + } const startWitnessCalculation = Date.now(); const inputObj = { nullifiers, @@ -653,21 +806,21 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti inputSalts, inputOwnerPrivateKey: signer.formattedPrivateKey, root, - enabled: [nullifiers[0] !== 0n ? 1 : 0, nullifiers[1] !== 0n ? 1 : 0], + enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)), merkleProof, outputCommitments, outputValues, - outputSalts: outputs.map((output) => output.salt), + outputSalts: outputs.map((output) => output.salt || 0n), outputOwnerPublicKeys, authorityPublicKey: Authority.babyJubPublicKey, ...encryptInputs, }; - const witness = await circuit.calculateWTNSBin(inputObj, true); + const witness = await circuitToUse.calculateWTNSBin(inputObj, true); const timeWithnessCalculation = Date.now() - startWitnessCalculation; const startProofGeneration = Date.now(); const { proof, publicSignals } = (await groth16.prove( - provingKey, + provingKeyToUse, witness )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; const timeProofGeneration = Date.now() - startProofGeneration; @@ -680,7 +833,10 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti return { inputCommitments, outputCommitments, - encryptedValues: publicSignals.slice(0, 20), + encryptedValuesForReceiver: publicSignals.slice(0, 4), + encryptedValuesForRegulator: isBatch + ? publicSignals.slice(4, 68) + : publicSignals.slice(4, 20), encryptionNonce, encodedProof, }; @@ -688,28 +844,25 @@ describe('Zeto based fungible token with anonymity using nullifiers and encrypti async function sendTx( signer: User, - nullifiers: [BigNumberish, BigNumberish], - outputCommitments: [BigNumberish, BigNumberish], + nullifiers: BigNumberish[], + outputCommitments: BigNumberish[], root: BigNumberish, - encryptedValues: BigNumberish[], + encryptedValuesForReceiver: BigNumberish[], + encryptedValuesForRegulator: BigNumberish[], encryptionNonce: BigNumberish, encodedProof: any ) { const startTx = Date.now(); - const encryptedValuesForReceiver = encryptedValues.slice(0, 4); - const encryptedValuesForRegulator = encryptedValues.slice(4, 20); - const tx = await zeto - .connect(signer.signer) - .transfer( - nullifiers, - outputCommitments, - root, - encryptionNonce, - encryptedValuesForReceiver, - encryptedValuesForRegulator, - encodedProof, - '0x' - ); + const tx = await zeto.connect(signer.signer).transfer( + nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works + outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works + root, + encryptionNonce, + encryptedValuesForReceiver, + encryptedValuesForRegulator, + encodedProof, + '0x' + ); const results: ContractTransactionReceipt | null = await tx.wait(); console.log( `Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${ diff --git a/solidity/test/zeto_anon_nullifier.ts b/solidity/test/zeto_anon_nullifier.ts index 19f1f53..8ea8705 100644 --- a/solidity/test/zeto_anon_nullifier.ts +++ b/solidity/test/zeto_anon_nullifier.ts @@ -51,6 +51,7 @@ describe('Zeto based fungible token with anonymity using nullifiers without encr let utxo4: UTXO; let utxo7: UTXO; let circuit: any, provingKey: any; + let batchCircuit: any, batchProvingKey: any; let smtAlice: Merkletree; let smtBob: Merkletree; @@ -68,16 +69,19 @@ describe('Zeto based fungible token with anonymity using nullifiers without encr ({ deployer, zeto, erc20 } = await deployZeto('Zeto_AnonNullifier')); - circuit = await loadCircuit('anon_nullifier'); - ({ provingKeyFile: provingKey } = loadProvingKeys('anon_nullifier')); - const storage1 = new InMemoryDB(str2Bytes('')); smtAlice = new Merkletree(storage1, true, 64); const storage2 = new InMemoryDB(str2Bytes('')); smtBob = new Merkletree(storage2, true, 64); - }); + circuit = await loadCircuit('anon_nullifier'); + ({ provingKeyFile: provingKey } = loadProvingKeys('anon_nullifier')); + batchCircuit = await loadCircuit('anon_nullifier_batch'); + ({ provingKeyFile: batchProvingKey } = loadProvingKeys( + 'anon_nullifier_batch' + )); + }); it('onchain SMT root should be equal to the offchain SMT root', async function () { const root = await smtAlice.root(); const onchainRoot = await zeto.getRoot(); @@ -85,6 +89,88 @@ describe('Zeto based fungible token with anonymity using nullifiers without encr expect(root.string()).to.equal(onchainRoot.toString()); }); + it('(batch) mint to Alice and batch transfer 10 UTXOs honestly to Bob should succeed', async function () { + // first mint the tokens for batch testing + const inputUtxos = []; + const nullifiers = []; + for (let i = 0; i < 10; i++) { + // mint 10 utxos + const _utxo = newUTXO(1, Alice); + nullifiers.push(newNullifier(_utxo, Alice)); + inputUtxos.push(_utxo); + } + const mintResult = await doMint(zeto, deployer, inputUtxos); + + const mintEvents = parseUTXOEvents(zeto, mintResult); + const mintedHashes = mintEvents[0].outputs; + for (let i = 0; i < mintedHashes.length; i++) { + if (mintedHashes[i] !== 0) { + await smtAlice.add(mintedHashes[i], mintedHashes[i]); + await smtBob.add(mintedHashes[i], mintedHashes[i]); + } + } + // Alice generates inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const mtps = []; + for (let i = 0; i < inputUtxos.length; i++) { + const p = await smtAlice.generateCircomVerifierProof( + inputUtxos[i].hash, + root + ); + mtps.push(p.siblings.map((s) => s.bigInt())); + } + + // Alice proposes the output UTXOs, 1 utxo to bob, 2 utxos to alice + const _bOut1 = newUTXO(8, Bob); + const _bOut2 = newUTXO(1, Alice); + const _bOut3 = newUTXO(1, Alice); + const outputUtxos = [_bOut1, _bOut2, _bOut3]; + const outputOwners = [Bob, Alice, Alice]; + const inflatedOutputUtxos = [...outputUtxos]; + const inflatedOutputOwners = [...outputOwners]; + for (let i = 0; i < 10 - outputUtxos.length; i++) { + inflatedOutputUtxos.push(ZERO_UTXO); + inflatedOutputOwners.push(Bob); + } + // Alice transfers her UTXOs to Bob + const result = await doTransfer( + Alice, + inputUtxos, + nullifiers, + inflatedOutputUtxos, + root.bigInt(), + mtps, + inflatedOutputOwners + ); + + const signerAddress = await Alice.signer.getAddress(); + const events = parseUTXOEvents(zeto, result.txResult!); + expect(events[0].submitter).to.equal(signerAddress); + expect(events[0].inputs).to.deep.equal(nullifiers.map((n) => n.hash)); + + const incomingUTXOs: any = events[0].outputs; + // check the non-empty output hashes are correct + for (let i = 0; i < outputUtxos.length; i++) { + // Bob uses the information received from Alice to reconstruct the UTXO sent to him + const receivedValue = outputUtxos[i].value; + const receivedSalt = outputUtxos[i].salt; + const hash = Poseidon.poseidon4([ + BigInt(receivedValue), + receivedSalt, + outputOwners[i].babyJubPublicKey[0], + outputOwners[i].babyJubPublicKey[1], + ]); + expect(incomingUTXOs[i]).to.equal(hash); + await smtAlice.add(incomingUTXOs[i], incomingUTXOs[i]); + await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]); + } + + // check empty hashes are empty + for (let i = outputUtxos.length; i < 10; i++) { + expect(incomingUTXOs[i]).to.equal(0); + } + }); + it('mint ERC20 tokens to Alice to deposit to Zeto should succeed', async function () { const startingBalance = await erc20.balanceOf(Alice.ethAddress); const tx = await erc20.connect(deployer).mint(Alice.ethAddress, 100); @@ -507,8 +593,8 @@ describe('Zeto based fungible token with anonymity using nullifiers without encr merkleProofs: BigInt[][], owners: User[] ) { - let nullifiers: [BigNumberish, BigNumberish]; - let outputCommitments: [BigNumberish, BigNumberish]; + let nullifiers: BigNumberish[]; + let outputCommitments: BigNumberish[]; let encodedProof: any; const result = await prepareProof( signer, @@ -519,10 +605,9 @@ describe('Zeto based fungible token with anonymity using nullifiers without encr merkleProofs, owners ); - nullifiers = _nullifiers.map((nullifier) => nullifier.hash) as [ - BigNumberish, - BigNumberish - ]; + nullifiers = _nullifiers.map( + (nullifier) => nullifier.hash + ) as BigNumberish[]; outputCommitments = result.outputCommitments; encodedProof = result.encodedProof; @@ -550,22 +635,18 @@ describe('Zeto based fungible token with anonymity using nullifiers without encr BigNumberish, BigNumberish ]; - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map( + const inputCommitments: BigNumberish[] = inputs.map( (input) => input.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); - const outputCommitments: [BigNumberish, BigNumberish] = outputs.map( + const outputCommitments: BigNumberish[] = outputs.map( (output) => output.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const outputValues = outputs.map((output) => BigInt(output.value || 0n)); - const outputOwnerPublicKeys: [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ] = owners.map((owner) => owner.babyJubPublicKey) as [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ]; + const outputOwnerPublicKeys: BigNumberish[][] = owners.map( + (owner) => owner.babyJubPublicKey + ) as BigNumberish[][]; const startWitnessCalculation = Date.now(); const inputObj = { @@ -575,19 +656,27 @@ describe('Zeto based fungible token with anonymity using nullifiers without encr inputSalts, inputOwnerPrivateKey: signer.formattedPrivateKey, root, - enabled: [nullifiers[0] !== 0n ? 1 : 0, nullifiers[1] !== 0n ? 1 : 0], + enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)), merkleProof, outputCommitments, outputValues, - outputSalts: outputs.map((output) => output.salt), + outputSalts: outputs.map((output) => output.salt || 0n), outputOwnerPublicKeys, }; - const witness = await circuit.calculateWTNSBin(inputObj, true); + + let circuitToUse = circuit; + let provingKeyToUse = provingKey; + if (inputCommitments.length > 2 || outputCommitments.length > 2) { + circuitToUse = batchCircuit; + provingKeyToUse = batchProvingKey; + } + + const witness = await circuitToUse.calculateWTNSBin(inputObj, true); const timeWithnessCalculation = Date.now() - startWitnessCalculation; const startProofGeneration = Date.now(); const { proof, publicSignals } = (await groth16.prove( - provingKey, + provingKeyToUse, witness )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; const timeProofGeneration = Date.now() - startProofGeneration; @@ -606,15 +695,19 @@ describe('Zeto based fungible token with anonymity using nullifiers without encr async function sendTx( signer: User, - nullifiers: [BigNumberish, BigNumberish], - outputCommitments: [BigNumberish, BigNumberish], + nullifiers: BigNumberish[], + outputCommitments: BigNumberish[], root: BigNumberish, encodedProof: any ) { const startTx = Date.now(); - const tx = await zeto - .connect(signer.signer) - .transfer(nullifiers, outputCommitments, root, encodedProof, '0x'); + const tx = await zeto.connect(signer.signer).transfer( + nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works + outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works + root, + encodedProof, + '0x' + ); const results: ContractTransactionReceipt | null = await tx.wait(); console.log( `Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${ diff --git a/solidity/test/zeto_anon_nullifier_kyc.ts b/solidity/test/zeto_anon_nullifier_kyc.ts index 62a9be7..3b763ad 100644 --- a/solidity/test/zeto_anon_nullifier_kyc.ts +++ b/solidity/test/zeto_anon_nullifier_kyc.ts @@ -56,9 +56,11 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou let utxo7: UTXO; let withdrawChangeUTXO: UTXO; let circuit: any, provingKey: any; + let batchCircuit: any, batchProvingKey: any; let smtAlice: Merkletree; let smtBob: Merkletree; let smtKyc: Merkletree; + let smtUnregistered: Merkletree; before(async function () { if (network.name !== 'hardhat') { @@ -81,9 +83,6 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou const tx4 = await zeto.connect(deployer).register(Charlie.babyJubPublicKey); const result3 = await tx4.wait(); - circuit = await loadCircuit('anon_nullifier_kyc'); - ({ provingKeyFile: provingKey } = loadProvingKeys('anon_nullifier_kyc')); - const storage1 = new InMemoryDB(str2Bytes('alice')); smtAlice = new Merkletree(storage1, true, 64); @@ -93,12 +92,22 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou const storage3 = new InMemoryDB(str2Bytes('kyc')); smtKyc = new Merkletree(storage3, true, 10); + const storage4 = new InMemoryDB(str2Bytes('unregistered')); + smtUnregistered = new Merkletree(storage4, true, 64); + const publicKey1 = parseRegistryEvents(zeto, result1); await smtKyc.add(kycHash(publicKey1), kycHash(publicKey1)); const publicKey2 = parseRegistryEvents(zeto, result2); await smtKyc.add(kycHash(publicKey2), kycHash(publicKey2)); const publicKey3 = parseRegistryEvents(zeto, result3); await smtKyc.add(kycHash(publicKey3), kycHash(publicKey3)); + + circuit = await loadCircuit('anon_nullifier_kyc'); + ({ provingKeyFile: provingKey } = loadProvingKeys('anon_nullifier_kyc')); + batchCircuit = await loadCircuit('anon_nullifier_kyc_batch'); + ({ provingKeyFile: batchProvingKey } = loadProvingKeys( + 'anon_nullifier_kyc_batch' + )); }); it('onchain SMT root should be equal to the offchain SMT root', async function () { @@ -108,6 +117,107 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou expect(root.string()).to.equal(onchainRoot.toString()); }); + it('(batch) mint to Alice and batch transfer 10 UTXOs honestly to Bob should succeed', async function () { + // first mint the tokens for batch testing + const inputUtxos = []; + const nullifiers = []; + for (let i = 0; i < 10; i++) { + // mint 10 utxos + const _utxo = newUTXO(1, Alice); + nullifiers.push(newNullifier(_utxo, Alice)); + inputUtxos.push(_utxo); + } + const mintResult = await doMint(zeto, deployer, inputUtxos); + + const mintEvents = parseUTXOEvents(zeto, mintResult); + const mintedHashes = mintEvents[0].outputs; + for (let i = 0; i < mintedHashes.length; i++) { + if (mintedHashes[i] !== 0) { + await smtAlice.add(mintedHashes[i], mintedHashes[i]); + await smtBob.add(mintedHashes[i], mintedHashes[i]); + await smtUnregistered.add(mintedHashes[i], mintedHashes[i]); + } + } + // Alice generates inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const mtps = []; + for (let i = 0; i < inputUtxos.length; i++) { + const p = await smtAlice.generateCircomVerifierProof( + inputUtxos[i].hash, + root + ); + mtps.push(p.siblings.map((s) => s.bigInt())); + } + + // Alice generates inclusion proofs for the identities in the transaction + const identitiesRoot = await smtKyc.root(); + const aProof = await smtKyc.generateCircomVerifierProof( + kycHash(Alice.babyJubPublicKey), + identitiesRoot + ); + const aliceProof = aProof.siblings.map((s) => s.bigInt()); + const bProof = await smtKyc.generateCircomVerifierProof( + kycHash(Bob.babyJubPublicKey), + identitiesRoot + ); + const bobProof = bProof.siblings.map((s) => s.bigInt()); + // Alice proposes the output UTXOs, 1 utxo to bob, 2 utxos to alice + const _bOut1 = newUTXO(8, Bob); + const _bOut2 = newUTXO(1, Alice); + const _bOut3 = newUTXO(1, Alice); + const outputUtxos = [_bOut1, _bOut2, _bOut3]; + const outputOwners = [Bob, Alice, Alice]; + const identityMerkleProofs = [aliceProof, bobProof, aliceProof, aliceProof]; + const inflatedOutputUtxos = [...outputUtxos]; + const inflatedOutputOwners = [...outputOwners]; + for (let i = 0; i < 10 - outputUtxos.length; i++) { + inflatedOutputUtxos.push(ZERO_UTXO); + inflatedOutputOwners.push(Bob); + identityMerkleProofs.push(bobProof); + } + + // Alice transfers her UTXOs to Bob + const result = await doTransfer( + Alice, + inputUtxos, + nullifiers, + inflatedOutputUtxos, + root.bigInt(), + mtps, + identitiesRoot.bigInt(), + identityMerkleProofs, + inflatedOutputOwners + ); + + const signerAddress = await Alice.signer.getAddress(); + const events = parseUTXOEvents(zeto, result.txResult!); + expect(events[0].submitter).to.equal(signerAddress); + expect(events[0].inputs).to.deep.equal(nullifiers.map((n) => n.hash)); + + const incomingUTXOs: any = events[0].outputs; + // check the non-empty output hashes are correct + for (let i = 0; i < outputUtxos.length; i++) { + // Bob uses the information received from Alice to reconstruct the UTXO sent to him + const receivedValue = outputUtxos[i].value; + const receivedSalt = outputUtxos[i].salt; + const hash = Poseidon.poseidon4([ + BigInt(receivedValue), + receivedSalt, + outputOwners[i].babyJubPublicKey[0], + outputOwners[i].babyJubPublicKey[1], + ]); + expect(incomingUTXOs[i]).to.equal(hash); + await smtAlice.add(incomingUTXOs[i], incomingUTXOs[i]); + await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]); + await smtUnregistered.add(incomingUTXOs[i], incomingUTXOs[i]); + } + + // check empty hashes are empty + for (let i = outputUtxos.length; i < 10; i++) { + expect(incomingUTXOs[i]).to.equal(0); + } + }); + it('mint ERC20 tokens to Alice to deposit to Zeto should succeed', async function () { const startingBalance = await erc20.balanceOf(Alice.ethAddress); const tx = await erc20.connect(deployer).mint(Alice.ethAddress, 100); @@ -357,15 +467,8 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou }); describe('unregistered user flows', function () { - let storage3; - let smtUnregistered: Merkletree; let unregisteredUtxo100: UTXO; - before(() => { - storage3 = new InMemoryDB(str2Bytes('unregistered')); - smtUnregistered = new Merkletree(storage3, true, 64); - }); - it('deposit by an unregistered user should succeed', async function () { const tx = await erc20 .connect(deployer) @@ -811,8 +914,8 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou identitiesMerkleProof: BigInt[][], owners: User[] ) { - let nullifiers: [BigNumberish, BigNumberish]; - let outputCommitments: [BigNumberish, BigNumberish]; + let nullifiers: BigNumberish[]; + let outputCommitments: BigNumberish[]; let encodedProof: any; const result = await prepareProof( signer, @@ -858,22 +961,18 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou BigNumberish, BigNumberish ]; - const inputCommitments: [BigNumberish, BigNumberish] = inputs.map( + const inputCommitments: BigNumberish[] = inputs.map( (input) => input.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const inputValues = inputs.map((input) => BigInt(input.value || 0n)); const inputSalts = inputs.map((input) => input.salt || 0n); - const outputCommitments: [BigNumberish, BigNumberish] = outputs.map( + const outputCommitments: BigNumberish[] = outputs.map( (output) => output.hash - ) as [BigNumberish, BigNumberish]; + ) as BigNumberish[]; const outputValues = outputs.map((output) => BigInt(output.value || 0n)); - const outputOwnerPublicKeys: [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ] = owners.map((owner) => owner.babyJubPublicKey) as [ - [BigNumberish, BigNumberish], - [BigNumberish, BigNumberish] - ]; + const outputOwnerPublicKeys: BigNumberish[][] = owners.map( + (owner) => owner.babyJubPublicKey + ) as BigNumberish[][]; const startWitnessCalculation = Date.now(); const inputObj = { nullifiers, @@ -882,7 +981,7 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou inputSalts, inputOwnerPrivateKey: signer.formattedPrivateKey, utxosRoot, - enabled: [nullifiers[0] !== 0n ? 1 : 0, nullifiers[1] !== 0n ? 1 : 0], + enabled: nullifiers.map((n) => (n !== 0n ? 1 : 0)), utxosMerkleProof, identitiesRoot, identitiesMerkleProof, @@ -891,12 +990,20 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou outputSalts: outputs.map((output) => output.salt || 0n), outputOwnerPublicKeys, }; - const witness = await circuit.calculateWTNSBin(inputObj, true); + + let circuitToUse = circuit; + let provingKeyToUse = provingKey; + if (inputCommitments.length > 2 || outputCommitments.length > 2) { + circuitToUse = batchCircuit; + provingKeyToUse = batchProvingKey; + } + + const witness = await circuitToUse.calculateWTNSBin(inputObj, true); const timeWithnessCalculation = Date.now() - startWitnessCalculation; const startProofGeneration = Date.now(); const { proof, publicSignals } = (await groth16.prove( - provingKey, + provingKeyToUse, witness )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] }; const timeProofGeneration = Date.now() - startProofGeneration; @@ -915,15 +1022,19 @@ describe('Zeto based fungible token with anonymity, KYC, using nullifiers withou async function sendTx( signer: User, - nullifiers: [BigNumberish, BigNumberish], - outputCommitments: [BigNumberish, BigNumberish], + nullifiers: BigNumberish[], + outputCommitments: BigNumberish[], root: BigNumberish, encodedProof: any ) { const startTx = Date.now(); - const tx = await zeto - .connect(signer.signer) - .transfer(nullifiers, outputCommitments, root, encodedProof, '0x'); + const tx = await zeto.connect(signer.signer).transfer( + nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works + outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works + root, + encodedProof, + '0x' + ); const results: ContractTransactionReceipt | null = await tx.wait(); console.log( `Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${