diff --git a/go-sdk/internal/sparse-merkle-tree/smt/errors.go b/go-sdk/internal/sparse-merkle-tree/smt/errors.go index 5a799d0..43cce56 100644 --- a/go-sdk/internal/sparse-merkle-tree/smt/errors.go +++ b/go-sdk/internal/sparse-merkle-tree/smt/errors.go @@ -19,8 +19,8 @@ package smt import "errors" var ( - // ErrMaxLevelsExceeded is used when a level is larger than the max - ErrMaxLevelsExceeded = errors.New("tree height is larger than max allowed (256)") + // ErrMaxLevelsNotInRange is used when a level is larger than the max + ErrMaxLevelsNotInRange = errors.New("tree height must be larger than zero and less than max allowed (256)") // ErrNodeIndexAlreadyExists is used when a node index already exists. ErrNodeIndexAlreadyExists = errors.New("key already exists") // ErrKeyNotFound is used when a key is not found in the MerkleTree. diff --git a/go-sdk/internal/sparse-merkle-tree/smt/merkletree.go b/go-sdk/internal/sparse-merkle-tree/smt/merkletree.go index 4b6a5f9..e1f0a8b 100644 --- a/go-sdk/internal/sparse-merkle-tree/smt/merkletree.go +++ b/go-sdk/internal/sparse-merkle-tree/smt/merkletree.go @@ -38,8 +38,8 @@ type sparseMerkleTree struct { } func NewMerkleTree(db core.Storage, maxLevels int) (core.SparseMerkleTree, error) { - if maxLevels > MAX_TREE_HEIGHT { - return nil, ErrMaxLevelsExceeded + if maxLevels <= 0 || maxLevels > MAX_TREE_HEIGHT { + return nil, ErrMaxLevelsNotInRange } mt := sparseMerkleTree{db: db, maxLevels: maxLevels} diff --git a/go-sdk/internal/sparse-merkle-tree/smt/merkletree_test.go b/go-sdk/internal/sparse-merkle-tree/smt/merkletree_test.go new file mode 100644 index 0000000..5df24f3 --- /dev/null +++ b/go-sdk/internal/sparse-merkle-tree/smt/merkletree_test.go @@ -0,0 +1,67 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package smt + +import ( + "fmt" + "testing" + + "github.com/hyperledger-labs/zeto/go-sdk/internal/sparse-merkle-tree/storage" + "github.com/hyperledger-labs/zeto/go-sdk/pkg/sparse-merkle-tree/core" + "github.com/stretchr/testify/assert" +) + +type mockStorage struct { + GetRootNodeIndex_customError bool +} + +func (ms *mockStorage) GetRootNodeIndex() (core.NodeIndex, error) { + if ms.GetRootNodeIndex_customError { + return nil, fmt.Errorf("nasty error in get root") + } + return nil, storage.ErrNotFound +} +func (ms *mockStorage) UpsertRootNodeIndex(core.NodeIndex) error { + return fmt.Errorf("nasty error in upsert root") +} +func (ms *mockStorage) GetNode(core.NodeIndex) (core.Node, error) { + return nil, nil +} +func (ms *mockStorage) InsertNode(core.Node) error { + return nil +} +func (ms *mockStorage) Close() {} + +func TestNewMerkleTreeFailures(t *testing.T) { + db := &mockStorage{} + mt, err := NewMerkleTree(db, 0) + assert.EqualError(t, err, ErrMaxLevelsNotInRange.Error()) + assert.Nil(t, mt) + + mt, err = NewMerkleTree(nil, 257) + assert.Error(t, err, ErrMaxLevelsNotInRange.Error()) + assert.Nil(t, mt) + + mt, err = NewMerkleTree(db, 64) + assert.EqualError(t, err, "nasty error in upsert root") + assert.Nil(t, mt) + + db.GetRootNodeIndex_customError = true + mt, err = NewMerkleTree(db, 64) + assert.EqualError(t, err, "nasty error in get root") + assert.Nil(t, mt) +} diff --git a/solidity/.gitignore b/solidity/.gitignore index 63b34f4..7e3df9d 100644 --- a/solidity/.gitignore +++ b/solidity/.gitignore @@ -3,4 +3,5 @@ node_modules artifacts cache typechain-types -ignition/deployments \ No newline at end of file +ignition/deployments +.openzeppelin \ No newline at end of file diff --git a/solidity/scripts/deploy_cloneable.ts b/solidity/scripts/deploy_cloneable.ts index efa4756..54af872 100644 --- a/solidity/scripts/deploy_cloneable.ts +++ b/solidity/scripts/deploy_cloneable.ts @@ -8,14 +8,8 @@ export async function deployFungible(tokenName: string) { const { deployer, args, libraries } = await verifiersDeployer.deployDependencies(); let zetoFactory; - const opts = { - kind: 'uups', - initializer: 'initialize', - unsafeAllow: ['delegatecall'] - }; if (libraries) { zetoFactory = await getLinkedContractFactory(tokenName, libraries); - opts.unsafeAllow.push('external-library-linking'); } else { zetoFactory = await ethers.getContractFactory(tokenName) } @@ -24,6 +18,9 @@ export async function deployFungible(tokenName: string) { await zetoImpl.waitForDeployment(); await zetoImpl.connect(deployer).initialize(...args); + const tx3 = await zetoImpl.connect(deployer).setERC20(erc20.target); + await tx3.wait(); + console.log(`ERC20 deployed: ${erc20.target}`); console.log(`ZetoToken deployed: ${zetoImpl.target}`); @@ -36,14 +33,8 @@ export async function deployNonFungible(tokenName: string) { const { args, libraries } = await verifiersDeployer.deployDependencies(); let zetoFactory; - const opts = { - kind: 'uups', - initializer: 'initialize', - unsafeAllow: ['delegatecall'] - }; if (libraries) { zetoFactory = await getLinkedContractFactory(tokenName, libraries); - opts.unsafeAllow.push('external-library-linking'); } else { zetoFactory = await ethers.getContractFactory(tokenName) } diff --git a/solidity/test/lib/deploy.ts b/solidity/test/lib/deploy.ts index 26a2ff6..aa751b9 100644 --- a/solidity/test/lib/deploy.ts +++ b/solidity/test/lib/deploy.ts @@ -9,6 +9,15 @@ import { ethers } from 'hardhat'; export async function deployZeto(tokenName: string) { let zeto, erc20, deployer; + // for testing with public chains, skip deployment if + // the contract address is provided + if (process.env.ZETO_ADDRESS && process.env.ERC20_ADDRESS) { + zeto = await ethers.getContractAt(tokenName, process.env.ZETO_ADDRESS); + erc20 = await ethers.getContractAt('SampleERC20', process.env.ERC20_ADDRESS); + deployer = (await ethers.getSigners())[0]; + return { deployer, zeto, erc20 }; + } + let isFungible = false; const fungibility = (fungibilities as any)[tokenName]; if (fungibility === 'fungible') { @@ -28,6 +37,8 @@ export async function deployZeto(tokenName: string) { const result = await deployFunc(tokenName); ({ deployer, zetoImpl, erc20, args } = result as any); + // we want to test the effectiveness of the factory contract + // to create clones of the Zeto implementation contract const Factory = await ethers.getContractFactory("ZetoTokenFactory"); const factory = await Factory.deploy(); await factory.waitForDeployment(); @@ -49,6 +60,12 @@ export async function deployZeto(tokenName: string) { } } zeto = await ethers.getContractAt(tokenName, zetoAddress); + + // set the ERC20 token for the fungible Zeto token + if (isFungible) { + const tx3 = await zeto.connect(deployer).setERC20(erc20.target); + await tx3.wait(); + } } return { deployer, zeto, erc20 }; diff --git a/solidity/test/zeto_anon.ts b/solidity/test/zeto_anon.ts index 9d278fa..8d26543 100644 --- a/solidity/test/zeto_anon.ts +++ b/solidity/test/zeto_anon.ts @@ -14,8 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import hre from 'hardhat'; -const { ethers } = hre; +import { ethers, network } from 'hardhat'; import { Signer, BigNumberish, AddressLike, ZeroAddress } from 'ethers'; import { expect } from 'chai'; import { loadCircuit, encodeProof, Poseidon } from "zeto-js"; @@ -45,6 +44,10 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi let circuit: any, provingKey: any; before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } let [d, a, b, c] = await ethers.getSigners(); deployer = d; Alice = await newUser(a); @@ -53,18 +56,16 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi ({ deployer, zeto, erc20 } = await deployZeto('Zeto_Anon')); - const tx3 = await zeto.connect(deployer).setERC20(erc20.target); - await tx3.wait(); - circuit = await loadCircuit('anon'); ({ provingKeyFile: provingKey } = loadProvingKeys('anon')); }); 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); await tx.wait(); - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(100); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(100); const tx1 = await erc20.connect(Alice.signer).approve(zeto.target, 100); await tx1.wait(); @@ -117,6 +118,8 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi }); it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () { + const startingBalance = await erc20.balanceOf(Alice.ethAddress); + // Alice proposes the output ERC20 tokens const outputCommitment = newUTXO(20, Alice); @@ -127,44 +130,53 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi await tx.wait(); // Alice checks her ERC20 balance - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(80); - }); - - it("Alice attempting to withdraw spent UTXOs should fail", async function () { - // Alice proposes the output ERC20 tokens - const outputCommitment = newUTXO(90, Alice); - - const { inputCommitments, outputCommitments, encodedProof } = await prepareWithdrawProof(Alice, [utxo100, ZERO_UTXO], outputCommitment); - - await expect(zeto.connect(Alice.signer).withdraw(10, inputCommitments, outputCommitments[0], encodedProof)).rejectedWith("UTXOAlreadySpent"); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(80); }); - it("mint existing unspent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); - }); - - it("mint existing spent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadySpent"); - }); - - it("transfer non-existing UTXOs should fail", async function () { - const nonExisting1 = newUTXO(10, Alice); - const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); - await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nonExisting1, nonExisting2], [Alice, Alice])).rejectedWith("UTXONotMinted"); - }); - - it("transfer spent UTXOs should fail (double spend protection)", async function () { - // create outputs - const utxo5 = newUTXO(25, Bob); - const utxo6 = newUTXO(5, Alice, utxo5.salt); - await expect(doTransfer(Alice, [utxo1, utxo2], [utxo5, utxo6], [Bob, Alice])).rejectedWith("UTXOAlreadySpent") - }); + describe('failure cases', function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } - it("spend by using the same UTXO as both inputs should fail", async function () { - const utxo5 = newUTXO(20, Alice); - const utxo6 = newUTXO(10, Bob, utxo5.salt); - await expect(doTransfer(Bob, [utxo7, utxo7], [utxo5, utxo6], [Alice, Bob])).rejectedWith(`UTXODuplicate(${utxo7.hash.toString()}`); + it("Alice attempting to withdraw spent UTXOs should fail", async function () { + // Alice proposes the output ERC20 tokens + const outputCommitment = newUTXO(90, Alice); + + const { inputCommitments, outputCommitments, encodedProof } = await prepareWithdrawProof(Alice, [utxo100, ZERO_UTXO], outputCommitment); + + await expect(zeto.connect(Alice.signer).withdraw(10, inputCommitments, outputCommitments[0], encodedProof)).rejectedWith("UTXOAlreadySpent"); + }); + + it("mint existing unspent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); + }); + + it("mint existing spent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadySpent"); + }); + + it("transfer non-existing UTXOs should fail", async function () { + const nonExisting1 = newUTXO(10, Alice); + const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); + await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nonExisting1, nonExisting2], [Alice, Alice])).rejectedWith("UTXONotMinted"); + }); + + it("transfer spent UTXOs should fail (double spend protection)", async function () { + // create outputs + const utxo5 = newUTXO(25, Bob); + const utxo6 = newUTXO(5, Alice, utxo5.salt); + await expect(doTransfer(Alice, [utxo1, utxo2], [utxo5, utxo6], [Bob, Alice])).rejectedWith("UTXOAlreadySpent") + }); + + it("spend by using the same UTXO as both inputs should fail", async function () { + const utxo5 = newUTXO(20, Alice); + const utxo6 = newUTXO(10, Bob, utxo5.salt); + await expect(doTransfer(Bob, [utxo7, utxo7], [utxo5, utxo6], [Alice, Bob])).rejectedWith(`UTXODuplicate(${utxo7.hash.toString()}`); + }); }); async function doTransfer(signer: User, inputs: UTXO[], outputs: UTXO[], owners: User[]) { diff --git a/solidity/test/zeto_anon_enc.ts b/solidity/test/zeto_anon_enc.ts index 1faf63b..7a9670a 100644 --- a/solidity/test/zeto_anon_enc.ts +++ b/solidity/test/zeto_anon_enc.ts @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ethers } from 'hardhat'; +import { ethers, network } from 'hardhat'; import { ContractTransactionReceipt, Signer, BigNumberish } from 'ethers'; import { expect } from 'chai'; import { loadCircuit, poseidonDecrypt, encodeProof, Poseidon } from "zeto-js"; @@ -41,6 +41,10 @@ describe("Zeto based fungible token with anonymity and encryption", function () let circuit: any, provingKey: any; before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } let [d, a, b, c] = await ethers.getSigners(); deployer = d; Alice = await newUser(a); @@ -49,18 +53,16 @@ describe("Zeto based fungible token with anonymity and encryption", function () ({ deployer, zeto, erc20 } = await deployZeto('Zeto_AnonEnc')); - const tx4 = await zeto.connect(deployer).setERC20(erc20.target); - await tx4.wait(); - circuit = await loadCircuit('anon_enc'); ({ provingKeyFile: provingKey } = loadProvingKeys('anon_enc')); }); 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); await tx.wait(); - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(100); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(100); const tx1 = await erc20.connect(Alice.signer).approve(zeto.target, 100); await tx1.wait(); @@ -113,6 +115,7 @@ describe("Zeto based fungible token with anonymity and encryption", function () }); it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () { + const startingBalance = await erc20.balanceOf(Alice.ethAddress); // Alice proposes the output ERC20 tokens const outputCommitment = newUTXO(20, Alice); @@ -123,48 +126,57 @@ describe("Zeto based fungible token with anonymity and encryption", function () await tx.wait(); // Alice checks her ERC20 balance - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(80); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(80); }); - it("Alice attempting to withdraw spent UTXOs should fail", async function () { - // Alice proposes the output ERC20 tokens - const outputCommitment = newUTXO(90, Alice); + describe("failure cases", function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } - const { inputCommitments, outputCommitments, encodedProof } = await prepareWithdrawProof(Alice, [utxo100, ZERO_UTXO], outputCommitment); + it("Alice attempting to withdraw spent UTXOs should fail", async function () { + // Alice proposes the output ERC20 tokens + const outputCommitment = newUTXO(90, Alice); - await expect(zeto.connect(Alice.signer).withdraw(10, inputCommitments, outputCommitments[0], encodedProof)).rejectedWith("UTXOAlreadySpent"); - }); + const { inputCommitments, outputCommitments, encodedProof } = await prepareWithdrawProof(Alice, [utxo100, ZERO_UTXO], outputCommitment); - it("mint existing unspent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); - }); + await expect(zeto.connect(Alice.signer).withdraw(10, inputCommitments, outputCommitments[0], encodedProof)).rejectedWith("UTXOAlreadySpent"); + }); - it("mint existing spent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadySpent"); - }); + it("mint existing unspent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); + }); - it("transfer non-existing UTXOs should fail", async function () { - const nonExisting1 = newUTXO(10, Alice); - const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); - await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nonExisting1, nonExisting2], [Alice, Alice])).rejectedWith("UTXONotMinted"); - }); + it("mint existing spent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadySpent"); + }); - it("transfer spent UTXOs should fail (double spend protection)", async function () { - // create outputs - const _utxo1 = newUTXO(25, Bob); - const _utxo2 = newUTXO(5, Alice); - await expect(doTransfer(Alice, [utxo1, utxo2], [_utxo1, _utxo2], [Bob, Alice])).rejectedWith("UTXOAlreadySpent") - }); + it("transfer non-existing UTXOs should fail", async function () { + const nonExisting1 = newUTXO(10, Alice); + const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); + await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nonExisting1, nonExisting2], [Alice, Alice])).rejectedWith("UTXONotMinted"); + }); + + it("transfer spent UTXOs should fail (double spend protection)", async function () { + // create outputs + const _utxo1 = newUTXO(25, Bob); + const _utxo2 = newUTXO(5, Alice); + await expect(doTransfer(Alice, [utxo1, utxo2], [_utxo1, _utxo2], [Bob, Alice])).rejectedWith("UTXOAlreadySpent") + }); - it("spend by using the same UTXO as both inputs should fail", async function () { - // mint a new UTXO to Bob - const _utxo1 = newUTXO(20, Bob); - await doMint(zeto, deployer, [_utxo1]); + it("spend by using the same UTXO as both inputs should fail", async function () { + // mint a new UTXO to Bob + const _utxo1 = newUTXO(20, Bob); + await doMint(zeto, deployer, [_utxo1]); - const _utxo2 = newUTXO(25, Alice); - const _utxo3 = newUTXO(15, Bob); - await expect(doTransfer(Bob, [_utxo1, _utxo1], [_utxo2, _utxo3], [Alice, Bob])).rejectedWith(`UTXODuplicate(${_utxo1.hash.toString()}`); + const _utxo2 = newUTXO(25, Alice); + const _utxo3 = newUTXO(15, Bob); + await expect(doTransfer(Bob, [_utxo1, _utxo1], [_utxo2, _utxo3], [Alice, Bob])).rejectedWith(`UTXODuplicate(${_utxo1.hash.toString()}`); + }); }); async function doTransfer(signer: User, inputs: UTXO[], outputs: UTXO[], owners: User[]) { diff --git a/solidity/test/zeto_anon_enc_nullifier.ts b/solidity/test/zeto_anon_enc_nullifier.ts index 43f1ea9..bfb2f47 100644 --- a/solidity/test/zeto_anon_enc_nullifier.ts +++ b/solidity/test/zeto_anon_enc_nullifier.ts @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ethers } from 'hardhat'; +import { ethers, network } from 'hardhat'; import { ContractTransactionReceipt, Signer, BigNumberish } from 'ethers'; import { expect } from 'chai'; import { loadCircuit, poseidonDecrypt, encodeProof } from "zeto-js"; @@ -43,6 +43,10 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti let smtBob: Merkletree; before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } let [d, a, b, c] = await ethers.getSigners(); deployer = d; Alice = await newUser(a); @@ -51,9 +55,6 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti ({ deployer, zeto, erc20 } = await deployZeto('Zeto_AnonEncNullifier')); - const tx4 = await zeto.connect(deployer).setERC20(erc20.target); - await tx4.wait(); - circuit = await loadCircuit('anon_enc_nullifier'); ({ provingKeyFile: provingKey } = loadProvingKeys('anon_enc_nullifier')); @@ -72,10 +73,11 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti }); 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); await tx.wait(); - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(100); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(100); const tx1 = await erc20.connect(Alice.signer).approve(zeto.target, 100); await tx1.wait(); @@ -178,6 +180,8 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti }).timeout(600000); it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () { + const startingBalance = await erc20.balanceOf(Alice.ethAddress); + // Alice generates the nullifiers for the UTXOs to be spent const nullifier1 = newNullifier(utxo100, Alice); @@ -197,109 +201,118 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti await tx.wait(); // Alice checks her ERC20 balance - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(80); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(80); }); - it("Alice attempting to withdraw spent UTXOs should fail", async function () { - // Alice generates the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(utxo100, Alice); - - // Alice generates inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(0n, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - - // Alice proposes the output ERC20 tokens - const outputCommitment = newUTXO(90, Alice); - - const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs); - - // Alice withdraws her UTXOs to ERC20 tokens - await expect(zeto.connect(Alice.signer).withdraw(10, nullifiers, outputCommitments[0], root.bigInt(), encodedProof)).rejectedWith("UTXOAlreadySpent"); - }); - - it("mint existing unspent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); - }); - - it("mint existing spent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadyOwned"); - }); - - it("transfer spent UTXOs should fail (double spend protection)", async function () { - // create outputs - const _utxo1 = newUTXO(25, Bob); - const _utxo2 = newUTXO(5, Alice); - - // generate the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(utxo1, Alice); - const nullifier2 = newNullifier(utxo2, Alice); - - // generate inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(utxo2.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + describe("failure cases", function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } - await expect(doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Bob, Alice])).rejectedWith("UTXOAlreadySpent") - }).timeout(600000); + it("Alice attempting to withdraw spent UTXOs should fail", async function () { + // Alice generates the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(utxo100, Alice); - it("transfer with existing UTXOs in the output should fail (mass conservation protection)", async function () { - // give Bob another UTXO to be able to spend - const _utxo1 = newUTXO(15, Bob); - await doMint(zeto, deployer, [_utxo1]); - await smtBob.add(_utxo1.hash, _utxo1.hash); - - const nullifier1 = newNullifier(utxo7, Bob); - const nullifier2 = newNullifier(_utxo1, Bob); - let root = await smtBob.root(); - const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); - const proof2 = await smtBob.generateCircomVerifierProof(_utxo1.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + // Alice generates inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(0n, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - await expect(doTransfer(Bob, [utxo7, _utxo1], [nullifier1, nullifier2], [utxo1, utxo2], root.bigInt(), merkleProofs, [Alice, Alice])).rejectedWith("UTXOAlreadyOwned") - }).timeout(600000); + // Alice proposes the output ERC20 tokens + const outputCommitment = newUTXO(90, Alice); - it("spend by using the same UTXO as both inputs should fail", async function () { - const _utxo1 = newUTXO(20, Alice); - const _utxo2 = newUTXO(10, Bob); - const nullifier1 = newNullifier(utxo7, Bob); - const nullifier2 = newNullifier(utxo7, Bob); - // generate inclusion proofs for the UTXOs to be spent - let root = await smtBob.root(); - const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); - const proof2 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs); - await expect(doTransfer(Bob, [utxo7, utxo7], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Alice, Bob])).rejectedWith(`UTXODuplicate`); - }).timeout(600000); - - it("transfer non-existing UTXOs should fail", async function () { - const nonExisting1 = newUTXO(25, Alice); - const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); - - // add to our local SMT (but they don't exist on the chain) - await smtAlice.add(nonExisting1.hash, nonExisting1.hash); - await smtAlice.add(nonExisting2.hash, nonExisting2.hash); - - // generate the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(nonExisting1, Alice); - const nullifier2 = newNullifier(nonExisting2, Alice); + // Alice withdraws her UTXOs to ERC20 tokens + await expect(zeto.connect(Alice.signer).withdraw(10, nullifiers, outputCommitments[0], root.bigInt(), encodedProof)).rejectedWith("UTXOAlreadySpent"); + }); - // generate inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(nonExisting1.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(nonExisting2.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + it("mint existing unspent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); + }); - // propose the output UTXOs - const _utxo1 = newUTXO(30, Charlie); - utxo7 = newUTXO(15, Bob); + it("mint existing spent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadyOwned"); + }); - await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); - }).timeout(600000); + it("transfer spent UTXOs should fail (double spend protection)", async function () { + // create outputs + const _utxo1 = newUTXO(25, Bob); + const _utxo2 = newUTXO(5, Alice); + + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(utxo1, Alice); + const nullifier2 = newNullifier(utxo2, Alice); + + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(utxo2.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Bob, Alice])).rejectedWith("UTXOAlreadySpent") + }).timeout(600000); + + it("transfer with existing UTXOs in the output should fail (mass conservation protection)", async function () { + // give Bob another UTXO to be able to spend + const _utxo1 = newUTXO(15, Bob); + await doMint(zeto, deployer, [_utxo1]); + await smtBob.add(_utxo1.hash, _utxo1.hash); + + const nullifier1 = newNullifier(utxo7, Bob); + const nullifier2 = newNullifier(_utxo1, Bob); + let root = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const proof2 = await smtBob.generateCircomVerifierProof(_utxo1.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Bob, [utxo7, _utxo1], [nullifier1, nullifier2], [utxo1, utxo2], root.bigInt(), merkleProofs, [Alice, Alice])).rejectedWith("UTXOAlreadyOwned") + }).timeout(600000); + + it("spend by using the same UTXO as both inputs should fail", async function () { + const _utxo1 = newUTXO(20, Alice); + const _utxo2 = newUTXO(10, Bob); + const nullifier1 = newNullifier(utxo7, Bob); + const nullifier2 = newNullifier(utxo7, Bob); + // generate inclusion proofs for the UTXOs to be spent + let root = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const proof2 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Bob, [utxo7, utxo7], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Alice, Bob])).rejectedWith(`UTXODuplicate`); + }).timeout(600000); + + it("transfer non-existing UTXOs should fail", async function () { + const nonExisting1 = newUTXO(25, Alice); + const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); + + // add to our local SMT (but they don't exist on the chain) + await smtAlice.add(nonExisting1.hash, nonExisting1.hash); + await smtAlice.add(nonExisting2.hash, nonExisting2.hash); + + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(nonExisting1, Alice); + const nullifier2 = newNullifier(nonExisting2, Alice); + + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(nonExisting1.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(nonExisting2.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + // propose the output UTXOs + const _utxo1 = newUTXO(30, Charlie); + utxo7 = newUTXO(15, Bob); + + await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); + }).timeout(600000); + }); async function doTransfer(signer: User, inputs: UTXO[], _nullifiers: UTXO[], outputs: UTXO[], root: BigInt, merkleProofs: BigInt[][], owners: User[]) { let nullifiers: [BigNumberish, BigNumberish]; diff --git a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts index 5752df2..0b42a5a 100644 --- a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts +++ b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ethers } from 'hardhat'; +import { ethers, network } from 'hardhat'; import { ContractTransactionReceipt, Signer, BigNumberish } from 'ethers'; import { expect } from 'chai'; import { loadCircuit, poseidonDecrypt, encodeProof, Poseidon } from "zeto-js"; @@ -44,6 +44,10 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti let smtBob: Merkletree; before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } let [d, a, b, c, e] = await ethers.getSigners(); deployer = d; Alice = await newUser(a); @@ -53,10 +57,8 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti ({ deployer, zeto, erc20 } = await deployZeto('Zeto_AnonEncNullifierNonRepudiation')); - const tx4 = await zeto.connect(deployer).setERC20(erc20.target); - await tx4.wait(); - const tx5 = await zeto.connect(deployer).setArbiter(Authority.babyJubPublicKey); - await tx5.wait(); + const tx1 = await zeto.connect(deployer).setArbiter(Authority.babyJubPublicKey); + await tx1.wait(); circuit = await loadCircuit('anon_enc_nullifier_non_repudiation'); ({ provingKeyFile: provingKey } = loadProvingKeys('anon_enc_nullifier_non_repudiation')); @@ -76,10 +78,11 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti }); 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); await tx.wait(); - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(100); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(100); const tx1 = await erc20.connect(Alice.signer).approve(zeto.target, 100); await tx1.wait(); @@ -214,6 +217,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti }).timeout(600000); it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () { + const startingBalance = await erc20.balanceOf(Alice.ethAddress); // Alice generates the nullifiers for the UTXOs to be spent const nullifier1 = newNullifier(utxo100, Alice); @@ -233,109 +237,118 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti await tx.wait(); // Alice checks her ERC20 balance - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(80); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(80); }); - it("Alice attempting to withdraw spent UTXOs should fail", async function () { - // Alice generates the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(utxo100, Alice); - - // Alice generates inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(0n, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - - // Alice proposes the output ERC20 tokens - const outputCommitment = newUTXO(20, Alice); - - const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs); - - // Alice withdraws her UTXOs to ERC20 tokens - await expect(zeto.connect(Alice.signer).withdraw(80, nullifiers, outputCommitments[0], root.bigInt(), encodedProof)).rejectedWith("UTXOAlreadySpent"); - }); - - it("mint existing unspent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); - }); - - it("mint existing spent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadyOwned"); - }); - - it("transfer spent UTXOs should fail (double spend protection)", async function () { - // create outputs - const _utxo1 = newUTXO(25, Bob); - const _utxo2 = newUTXO(5, Alice); - - // generate the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(utxo1, Alice); - const nullifier2 = newNullifier(utxo2, Alice); - - // generate inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(utxo2.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + describe("failure cases", function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } - await expect(doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Bob, Alice])).rejectedWith("UTXOAlreadySpent") - }).timeout(600000); + it("Alice attempting to withdraw spent UTXOs should fail", async function () { + // Alice generates the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(utxo100, Alice); - it("transfer with existing UTXOs in the output should fail (mass conservation protection)", async function () { - // give Bob another UTXO to be able to spend - const _utxo1 = newUTXO(15, Bob); - await doMint(zeto, deployer, [_utxo1]); - await smtBob.add(_utxo1.hash, _utxo1.hash); - - const nullifier1 = newNullifier(utxo7, Bob); - const nullifier2 = newNullifier(_utxo1, Bob); - let root = await smtBob.root(); - const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); - const proof2 = await smtBob.generateCircomVerifierProof(_utxo1.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + // Alice generates inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(0n, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - await expect(doTransfer(Bob, [utxo7, _utxo1], [nullifier1, nullifier2], [utxo1, utxo2], root.bigInt(), merkleProofs, [Alice, Alice])).rejectedWith("UTXOAlreadyOwned") - }).timeout(600000); + // Alice proposes the output ERC20 tokens + const outputCommitment = newUTXO(20, Alice); - it("spend by using the same UTXO as both inputs should fail", async function () { - const _utxo1 = newUTXO(20, Alice); - const _utxo2 = newUTXO(10, Bob); - const nullifier1 = newNullifier(utxo7, Bob); - const nullifier2 = newNullifier(utxo7, Bob); - // generate inclusion proofs for the UTXOs to be spent - let root = await smtBob.root(); - const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); - const proof2 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs); - await expect(doTransfer(Bob, [utxo7, utxo7], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Alice, Bob])).rejectedWith(`UTXODuplicate`); - }).timeout(600000); - - it("transfer non-existing UTXOs should fail", async function () { - const nonExisting1 = newUTXO(25, Alice); - const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); - - // add to our local SMT (but they don't exist on the chain) - await smtAlice.add(nonExisting1.hash, nonExisting1.hash); - await smtAlice.add(nonExisting2.hash, nonExisting2.hash); - - // generate the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(nonExisting1, Alice); - const nullifier2 = newNullifier(nonExisting2, Alice); + // Alice withdraws her UTXOs to ERC20 tokens + await expect(zeto.connect(Alice.signer).withdraw(80, nullifiers, outputCommitments[0], root.bigInt(), encodedProof)).rejectedWith("UTXOAlreadySpent"); + }); - // generate inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(nonExisting1.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(nonExisting2.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + it("mint existing unspent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); + }); - // propose the output UTXOs - const _utxo1 = newUTXO(30, Charlie); - utxo7 = newUTXO(15, Bob); + it("mint existing spent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadyOwned"); + }); - await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); - }).timeout(600000); + it("transfer spent UTXOs should fail (double spend protection)", async function () { + // create outputs + const _utxo1 = newUTXO(25, Bob); + const _utxo2 = newUTXO(5, Alice); + + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(utxo1, Alice); + const nullifier2 = newNullifier(utxo2, Alice); + + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(utxo2.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Bob, Alice])).rejectedWith("UTXOAlreadySpent") + }).timeout(600000); + + it("transfer with existing UTXOs in the output should fail (mass conservation protection)", async function () { + // give Bob another UTXO to be able to spend + const _utxo1 = newUTXO(15, Bob); + await doMint(zeto, deployer, [_utxo1]); + await smtBob.add(_utxo1.hash, _utxo1.hash); + + const nullifier1 = newNullifier(utxo7, Bob); + const nullifier2 = newNullifier(_utxo1, Bob); + let root = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const proof2 = await smtBob.generateCircomVerifierProof(_utxo1.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Bob, [utxo7, _utxo1], [nullifier1, nullifier2], [utxo1, utxo2], root.bigInt(), merkleProofs, [Alice, Alice])).rejectedWith("UTXOAlreadyOwned") + }).timeout(600000); + + it("spend by using the same UTXO as both inputs should fail", async function () { + const _utxo1 = newUTXO(20, Alice); + const _utxo2 = newUTXO(10, Bob); + const nullifier1 = newNullifier(utxo7, Bob); + const nullifier2 = newNullifier(utxo7, Bob); + // generate inclusion proofs for the UTXOs to be spent + let root = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const proof2 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Bob, [utxo7, utxo7], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Alice, Bob])).rejectedWith(`UTXODuplicate`); + }).timeout(600000); + + it("transfer non-existing UTXOs should fail", async function () { + const nonExisting1 = newUTXO(25, Alice); + const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); + + // add to our local SMT (but they don't exist on the chain) + await smtAlice.add(nonExisting1.hash, nonExisting1.hash); + await smtAlice.add(nonExisting2.hash, nonExisting2.hash); + + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(nonExisting1, Alice); + const nullifier2 = newNullifier(nonExisting2, Alice); + + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(nonExisting1.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(nonExisting2.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + // propose the output UTXOs + const _utxo1 = newUTXO(30, Charlie); + utxo7 = newUTXO(15, Bob); + + await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); + }).timeout(600000); + }); async function doTransfer(signer: User, inputs: UTXO[], _nullifiers: UTXO[], outputs: UTXO[], root: BigInt, merkleProofs: BigInt[][], owners: User[]) { let nullifiers: [BigNumberish, BigNumberish]; diff --git a/solidity/test/zeto_anon_nullifier.ts b/solidity/test/zeto_anon_nullifier.ts index f877dd1..8a2897b 100644 --- a/solidity/test/zeto_anon_nullifier.ts +++ b/solidity/test/zeto_anon_nullifier.ts @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ethers } from 'hardhat'; +import { ethers, network } from 'hardhat'; import { ContractTransactionReceipt, Signer, BigNumberish } from 'ethers'; import { expect } from 'chai'; import { loadCircuit, Poseidon, encodeProof } from "zeto-js"; @@ -42,6 +42,11 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr let smtBob: Merkletree; before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } + let [d, a, b, c] = await ethers.getSigners(); deployer = d; Alice = await newUser(a); @@ -50,9 +55,6 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr ({ deployer, zeto, erc20 } = await deployZeto('Zeto_AnonNullifier')); - const tx4 = await zeto.connect(deployer).setERC20(erc20.target); - await tx4.wait(); - circuit = await loadCircuit('anon_nullifier'); ({ provingKeyFile: provingKey } = loadProvingKeys('anon_nullifier')); @@ -71,10 +73,11 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr }); 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); await tx.wait(); - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(100); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(100); const tx1 = await erc20.connect(Alice.signer).approve(zeto.target, 100); await tx1.wait(); @@ -179,6 +182,8 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr }).timeout(600000); it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () { + const startingBalance = await erc20.balanceOf(Alice.ethAddress); + // Alice generates the nullifiers for the UTXOs to be spent const nullifier1 = newNullifier(utxo100, Alice); @@ -198,109 +203,118 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr await tx.wait(); // Alice checks her ERC20 balance - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(80); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(80); }); - it("Alice attempting to withdraw spent UTXOs should fail", async function () { - // Alice generates the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(utxo100, Alice); - - // Alice generates inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(0n, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - - // Alice proposes the output ERC20 tokens - const outputCommitment = newUTXO(90, Alice); - - const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs); - - await expect(zeto.connect(Alice.signer).withdraw(10, nullifiers, outputCommitments[0], root.bigInt(), encodedProof)).rejectedWith("UTXOAlreadySpent"); - }); - - it("mint existing unspent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); - }); - - it("mint existing spent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadyOwned"); + describe("failure cases", function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } + + it("Alice attempting to withdraw spent UTXOs should fail", async function () { + // Alice generates the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(utxo100, Alice); + + // Alice generates inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(0n, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + // Alice proposes the output ERC20 tokens + const outputCommitment = newUTXO(90, Alice); + + const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs); + + await expect(zeto.connect(Alice.signer).withdraw(10, nullifiers, outputCommitments[0], root.bigInt(), encodedProof)).rejectedWith("UTXOAlreadySpent"); + }); + + it("mint existing unspent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); + }); + + it("mint existing spent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadyOwned"); + }); + + it("transfer spent UTXOs should fail (double spend protection)", async function () { + // create outputs + const _utxo1 = newUTXO(25, Bob); + const _utxo2 = newUTXO(5, Alice); + + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(utxo1, Alice); + const nullifier2 = newNullifier(utxo2, Alice); + + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(utxo2.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Bob, Alice])).rejectedWith("UTXOAlreadySpent") + }).timeout(600000); + + it("transfer with existing UTXOs in the output should fail (mass conservation protection)", async function () { + // give Bob another UTXO to be able to spend + const _utxo1 = newUTXO(15, Bob); + await doMint(zeto, deployer, [_utxo1]); + await smtBob.add(_utxo1.hash, _utxo1.hash); + + const nullifier1 = newNullifier(utxo7, Bob); + const nullifier2 = newNullifier(_utxo1, Bob); + let root = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const proof2 = await smtBob.generateCircomVerifierProof(_utxo1.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Bob, [utxo7, _utxo1], [nullifier1, nullifier2], [utxo1, utxo2], root.bigInt(), merkleProofs, [Alice, Alice])).rejectedWith("UTXOAlreadyOwned") + }).timeout(600000); + + it("spend by using the same UTXO as both inputs should fail", async function () { + const _utxo1 = newUTXO(20, Alice); + const _utxo2 = newUTXO(10, Bob); + const nullifier1 = newNullifier(utxo7, Bob); + const nullifier2 = newNullifier(utxo7, Bob); + // generate inclusion proofs for the UTXOs to be spent + let root = await smtBob.root(); + const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const proof2 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + await expect(doTransfer(Bob, [utxo7, utxo7], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Alice, Bob])).rejectedWith(`UTXODuplicate`); + }).timeout(600000); + + it("transfer non-existing UTXOs should fail", async function () { + const nonExisting1 = newUTXO(25, Alice); + const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); + + // add to our local SMT (but they don't exist on the chain) + await smtAlice.add(nonExisting1.hash, nonExisting1.hash); + await smtAlice.add(nonExisting2.hash, nonExisting2.hash); + + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(nonExisting1, Alice); + const nullifier2 = newNullifier(nonExisting2, Alice); + + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(nonExisting1.hash, root); + const proof2 = await smtAlice.generateCircomVerifierProof(nonExisting2.hash, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + // propose the output UTXOs + const _utxo1 = newUTXO(30, Charlie); + utxo7 = newUTXO(15, Bob); + + await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); + }).timeout(600000); }); - it("transfer spent UTXOs should fail (double spend protection)", async function () { - // create outputs - const _utxo1 = newUTXO(25, Bob); - const _utxo2 = newUTXO(5, Alice); - - // generate the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(utxo1, Alice); - const nullifier2 = newNullifier(utxo2, Alice); - - // generate inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(utxo2.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - - await expect(doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Bob, Alice])).rejectedWith("UTXOAlreadySpent") - }).timeout(600000); - - it("transfer with existing UTXOs in the output should fail (mass conservation protection)", async function () { - // give Bob another UTXO to be able to spend - const _utxo1 = newUTXO(15, Bob); - await doMint(zeto, deployer, [_utxo1]); - await smtBob.add(_utxo1.hash, _utxo1.hash); - - const nullifier1 = newNullifier(utxo7, Bob); - const nullifier2 = newNullifier(_utxo1, Bob); - let root = await smtBob.root(); - const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); - const proof2 = await smtBob.generateCircomVerifierProof(_utxo1.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - - await expect(doTransfer(Bob, [utxo7, _utxo1], [nullifier1, nullifier2], [utxo1, utxo2], root.bigInt(), merkleProofs, [Alice, Alice])).rejectedWith("UTXOAlreadyOwned") - }).timeout(600000); - - it("spend by using the same UTXO as both inputs should fail", async function () { - const _utxo1 = newUTXO(20, Alice); - const _utxo2 = newUTXO(10, Bob); - const nullifier1 = newNullifier(utxo7, Bob); - const nullifier2 = newNullifier(utxo7, Bob); - // generate inclusion proofs for the UTXOs to be spent - let root = await smtBob.root(); - const proof1 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); - const proof2 = await smtBob.generateCircomVerifierProof(utxo7.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - - await expect(doTransfer(Bob, [utxo7, utxo7], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, [Alice, Bob])).rejectedWith(`UTXODuplicate`); - }).timeout(600000); - - it("transfer non-existing UTXOs should fail", async function () { - const nonExisting1 = newUTXO(25, Alice); - const nonExisting2 = newUTXO(20, Alice, nonExisting1.salt); - - // add to our local SMT (but they don't exist on the chain) - await smtAlice.add(nonExisting1.hash, nonExisting1.hash); - await smtAlice.add(nonExisting2.hash, nonExisting2.hash); - - // generate the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(nonExisting1, Alice); - const nullifier2 = newNullifier(nonExisting2, Alice); - - // generate inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(nonExisting1.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(nonExisting2.hash, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - - // propose the output UTXOs - const _utxo1 = newUTXO(30, Charlie); - utxo7 = newUTXO(15, Bob); - - await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); - }).timeout(600000); - async function doTransfer(signer: User, inputs: UTXO[], _nullifiers: UTXO[], outputs: UTXO[], root: BigInt, merkleProofs: BigInt[][], owners: User[]) { let nullifiers: [BigNumberish, BigNumberish]; let outputCommitments: [BigNumberish, BigNumberish]; diff --git a/solidity/test/zeto_anon_nullifier_kyc.ts b/solidity/test/zeto_anon_nullifier_kyc.ts index d192087..6f5f25c 100644 --- a/solidity/test/zeto_anon_nullifier_kyc.ts +++ b/solidity/test/zeto_anon_nullifier_kyc.ts @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ethers } from 'hardhat'; +import { ethers, network } from 'hardhat'; import { ContractTransactionReceipt, Signer, BigNumberish } from 'ethers'; import { expect } from 'chai'; import { loadCircuit, Poseidon, encodeProof, kycHash } from "zeto-js"; @@ -47,6 +47,10 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou let smtKyc: Merkletree; before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } let [d, a, b, c, e] = await ethers.getSigners(); deployer = d; Alice = await newUser(a); @@ -56,9 +60,6 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou ({ deployer, zeto, erc20 } = await deployZeto('Zeto_AnonNullifierKyc')); - const tx1 = await zeto.connect(deployer).setERC20(erc20.target); - await tx1.wait(); - const tx2 = await zeto.connect(deployer).register(Alice.babyJubPublicKey); const result1 = await tx2.wait(); const tx3 = await zeto.connect(deployer).register(Bob.babyJubPublicKey); @@ -94,10 +95,11 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou }); 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); await tx.wait(); - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(100); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(100); const tx1 = await erc20.connect(Alice.signer).approve(zeto.target, 100); await tx1.wait(); @@ -225,6 +227,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou }).timeout(600000); it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () { + const startingBalance = await erc20.balanceOf(Alice.ethAddress); // Alice generates the nullifiers for the UTXOs to be spent const nullifier1 = newNullifier(utxo100, Alice); @@ -243,8 +246,8 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou await tx.wait(); // Alice checks her ERC20 balance - const balance = await erc20.balanceOf(Alice.ethAddress); - expect(balance).to.equal(80); + const endingBalance = await erc20.balanceOf(Alice.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(80); }); describe("unregistered user flows", function () { @@ -302,6 +305,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou }); it("the unregistered user can still withdraw their UTXOs to ERC20 tokens", async function () { + const startingBalance = await erc20.balanceOf(unregistered.ethAddress); // unregistered user generates the nullifiers for the UTXOs to be spent const nullifier1 = newNullifier(unregisteredUtxo100, unregistered); @@ -321,12 +325,18 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou await tx.wait(); // unregistered user checks her ERC20 balance - const balance = await erc20.balanceOf(unregistered.ethAddress); - expect(balance).to.equal(100); + const endingBalance = await erc20.balanceOf(unregistered.ethAddress); + expect(endingBalance - startingBalance).to.be.equal(100); }); }); - describe("failure flows", function () { + describe("failure cases", function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } it("Alice attempting to withdraw spent UTXOs should fail", async function () { // Alice generates the nullifiers for the UTXOs to be spent diff --git a/solidity/test/zeto_nf_anon.ts b/solidity/test/zeto_nf_anon.ts index b1994ec..543fae0 100644 --- a/solidity/test/zeto_nf_anon.ts +++ b/solidity/test/zeto_nf_anon.ts @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ethers } from 'hardhat'; +import { ethers, network } from 'hardhat'; import { Signer, BigNumberish, AddressLike } from 'ethers'; import { expect } from 'chai'; import { loadCircuit, tokenUriHash, encodeProof } from "zeto-js"; @@ -36,6 +36,10 @@ describe("Zeto based non-fungible token with anonymity without encryption or nul let circuit: any, provingKey: any; before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } let [d, a, b, c] = await ethers.getSigners(); deployer = d; Alice = await newUser(a); @@ -72,25 +76,34 @@ describe("Zeto based non-fungible token with anonymity without encryption or nul await doTransfer(Bob, utxo2, utxo3, Charlie); }); - it("mint existing unspent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo3])).rejectedWith("UTXOAlreadyOwned"); - }); - - it("mint existing spent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadySpent"); - }); - - it("transfer non-existing UTXOs should fail", async function () { - const nonExisting1 = newAssetUTXO(1002, 'http://ipfs.io/file-hash-2', Alice); - const nonExisting2 = newAssetUTXO(1002, 'http://ipfs.io/file-hash-2', Bob); - - await expect(doTransfer(Alice, nonExisting1, nonExisting2, Bob)).rejectedWith("UTXONotMinted"); - }); - - it("transfer spent UTXOs should fail (double spend protection)", async function () { - // create outputs - const _utxo4 = newAssetUTXO(utxo1.tokenId!, utxo1.uri!, Bob); - await expect(doTransfer(Alice, utxo1, _utxo4, Bob)).rejectedWith("UTXOAlreadySpent") + describe("failure cases", function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } + + it("mint existing unspent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo3])).rejectedWith("UTXOAlreadyOwned"); + }); + + it("mint existing spent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadySpent"); + }); + + it("transfer non-existing UTXOs should fail", async function () { + const nonExisting1 = newAssetUTXO(1002, 'http://ipfs.io/file-hash-2', Alice); + const nonExisting2 = newAssetUTXO(1002, 'http://ipfs.io/file-hash-2', Bob); + + await expect(doTransfer(Alice, nonExisting1, nonExisting2, Bob)).rejectedWith("UTXONotMinted"); + }); + + it("transfer spent UTXOs should fail (double spend protection)", async function () { + // create outputs + const _utxo4 = newAssetUTXO(utxo1.tokenId!, utxo1.uri!, Bob); + await expect(doTransfer(Alice, utxo1, _utxo4, Bob)).rejectedWith("UTXOAlreadySpent") + }); }); async function doTransfer(signer: User, input: UTXO, output: UTXO, to: User) { diff --git a/solidity/test/zeto_nf_anon_nullifier.ts b/solidity/test/zeto_nf_anon_nullifier.ts index 9829a31..8b2a33f 100644 --- a/solidity/test/zeto_nf_anon_nullifier.ts +++ b/solidity/test/zeto_nf_anon_nullifier.ts @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ethers } from 'hardhat'; +import { ethers, network } from 'hardhat'; import { ContractTransactionReceipt, Signer, BigNumberish } from 'ethers'; import { expect } from 'chai'; import { loadCircuit, Poseidon, encodeProof, tokenUriHash } from "zeto-js"; @@ -37,6 +37,10 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without let smtBob: Merkletree; before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } let [d, a, b, c] = await ethers.getSigners(); deployer = d; Alice = await newUser(a); @@ -143,48 +147,57 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without await smtAlice.add(events[0].outputs[0], events[0].outputs[0]); }).timeout(600000); - it("mint existing unspent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo3])).rejectedWith("UTXOAlreadyOwned"); - }); + describe("failure cases", function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } - it("mint existing spent UTXOs should fail", async function () { - await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadyOwned"); - }); + it("mint existing unspent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo3])).rejectedWith("UTXOAlreadyOwned"); + }); - it("transfer spent UTXOs should fail (double spend protection)", async function () { - // Alice create outputs in an attempt to send to Charlie an already spent asset - const _utxo1 = newAssetUTXO(utxo1.tokenId!, utxo1.uri!, Charlie); + it("mint existing spent UTXOs should fail", async function () { + await expect(doMint(zeto, deployer, [utxo1])).rejectedWith("UTXOAlreadyOwned"); + }); - // generate the nullifiers for the UTXOs to be spent - const nullifier1 = newAssetNullifier(utxo1, Alice); + it("transfer spent UTXOs should fail (double spend protection)", async function () { + // Alice create outputs in an attempt to send to Charlie an already spent asset + const _utxo1 = newAssetUTXO(utxo1.tokenId!, utxo1.uri!, Charlie); - // generate inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); - const merkleProof = proof1.siblings.map((s) => s.bigInt()); + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newAssetNullifier(utxo1, Alice); - await expect(doTransfer(Alice, utxo1, nullifier1, _utxo1, root.bigInt(), merkleProof, Charlie)).rejectedWith("UTXOAlreadySpent") - }).timeout(600000); + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root); + const merkleProof = proof1.siblings.map((s) => s.bigInt()); - it("transfer non-existing UTXOs should fail", async function () { - const nonExisting1 = newAssetUTXO(1002, 'http://ipfs.io/file-hash-2', Alice); + await expect(doTransfer(Alice, utxo1, nullifier1, _utxo1, root.bigInt(), merkleProof, Charlie)).rejectedWith("UTXOAlreadySpent") + }).timeout(600000); - // add to our local SMT (but they don't exist on the chain) - await smtAlice.add(nonExisting1.hash, nonExisting1.hash); + it("transfer non-existing UTXOs should fail", async function () { + const nonExisting1 = newAssetUTXO(1002, 'http://ipfs.io/file-hash-2', Alice); - // generate the nullifiers for the UTXOs to be spent - const nullifier1 = newAssetNullifier(nonExisting1, Alice); + // add to our local SMT (but they don't exist on the chain) + await smtAlice.add(nonExisting1.hash, nonExisting1.hash); - // generate inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(nonExisting1.hash, root); - const merkleProof = proof1.siblings.map((s) => s.bigInt()); + // generate the nullifiers for the UTXOs to be spent + const nullifier1 = newAssetNullifier(nonExisting1, Alice); - // propose the output UTXOs - const _utxo1 = newAssetUTXO(nonExisting1.tokenId!, nonExisting1.uri!, Charlie); + // generate inclusion proofs for the UTXOs to be spent + let root = await smtAlice.root(); + const proof1 = await smtAlice.generateCircomVerifierProof(nonExisting1.hash, root); + const merkleProof = proof1.siblings.map((s) => s.bigInt()); - await expect(doTransfer(Alice, nonExisting1, nullifier1, _utxo1, root.bigInt(), merkleProof, Charlie)).rejectedWith("UTXORootNotFound"); - }).timeout(600000); + // propose the output UTXOs + const _utxo1 = newAssetUTXO(nonExisting1.tokenId!, nonExisting1.uri!, Charlie); + + await expect(doTransfer(Alice, nonExisting1, nullifier1, _utxo1, root.bigInt(), merkleProof, Charlie)).rejectedWith("UTXORootNotFound"); + }).timeout(600000); + }); async function doTransfer(signer: User, input: UTXO, _nullifier: UTXO, output: UTXO, root: BigInt, merkleProof: BigInt[], owner: User) { let nullifier: BigNumberish; diff --git a/solidity/test/zkDvP.ts b/solidity/test/zkDvP.ts index 897c102..9159fe4 100644 --- a/solidity/test/zkDvP.ts +++ b/solidity/test/zkDvP.ts @@ -14,12 +14,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ethers, ignition } from 'hardhat'; -import { Signer, BigNumberish, encodeBytes32String, ZeroHash } from 'ethers'; +import { ethers, ignition, network } from 'hardhat'; +import { Signer, encodeBytes32String, ZeroHash } from 'ethers'; import { expect } from 'chai'; import { loadCircuit, getProofHash } from "zeto-js"; -import zetoAnonModule from '../ignition/modules/zeto_anon'; -import zetoNFAnonModule from '../ignition/modules/zeto_nf_anon'; import zkDvPModule from '../ignition/modules/zkDvP'; import zetoAnonTests from './zeto_anon'; import zetoNFAnonTests from './zeto_nf_anon'; @@ -49,6 +47,10 @@ describe("DvP flows between fungible and non-fungible tokens based on Zeto with let deployer: Signer; before(async function () { + if (network.name !== 'hardhat') { + // accommodate for longer block times on public networks + this.timeout(120000); + } let [d, a, b, c] = await ethers.getSigners(); deployer = d; Alice = await newUser(a); @@ -85,29 +87,6 @@ describe("DvP flows between fungible and non-fungible tokens based on Zeto with expect(event.args.outputs[1].toString()).to.equal(asset2.hash.toString()); }); - it("Initiating a DvP transaction without payment input or asset input should fail", async function () { - await expect(zkDvP.connect(Alice.signer).initiateTrade([0, 0], [0, 0], ZeroHash, 0, 0, ZeroHash)).rejectedWith("Payment inputs and asset input cannot be zero at the same time"); - }); - - it("Initiating a DvP transaction with payment input but no payment output should fail", async function () { - const utxo1 = newUTXO(10, Alice); - const utxo2 = newUTXO(20, Alice); - await expect(zkDvP.connect(Alice.signer).initiateTrade([utxo1.hash, utxo2.hash], [0, 0], ZeroHash, 0, 0, ZeroHash)).rejectedWith("Payment outputs cannot be zero when payment inputs are non-zero"); - }); - - it("Initiating a DvP transaction with payment inputs and asset inputs should fail", async function () { - const utxo1 = newUTXO(10, Alice); - const utxo2 = newUTXO(20, Alice); - const utxo3 = newUTXO(25, Bob); - const utxo4 = newUTXO(5, Alice); - await expect(zkDvP.connect(Alice.signer).initiateTrade([utxo1.hash, utxo2.hash], [utxo3.hash, utxo4.hash], ZeroHash, utxo3.hash, 0, ZeroHash)).rejectedWith("Payment inputs and asset input cannot be provided at the same time"); - }); - - it("Initiating a DvP transaction with asset input but no asset output should fail", async function () { - const utxo1 = newUTXO(10, Alice); - await expect(zkDvP.connect(Alice.signer).initiateTrade([0, 0], [0, 0], ZeroHash, utxo1.hash, 0, ZeroHash)).rejectedWith("Asset output cannot be zero when asset input is non-zero"); - }); - it("Initiating a successful DvP transaction with payment inputs", async function () { const utxo1 = newUTXO(10, Alice); const utxo2 = newUTXO(20, Alice); @@ -122,45 +101,6 @@ describe("DvP flows between fungible and non-fungible tokens based on Zeto with await expect(zkDvP.connect(Alice.signer).initiateTrade([0, 0], [0, 0], ZeroHash, utxo1.hash, utxo2.hash, ZeroHash)).fulfilled; }); - it("Accepting a trade using an invalid trade ID should fail", async function () { - await expect(zkDvP.connect(Bob.signer).acceptTrade(1000, [0, 0], [0, 0], ZeroHash, 0, 0, ZeroHash)).rejectedWith("Trade does not exist"); - }); - - it("Failing cases for accepting a trade with payment terms", async function () { - const mockProofHash = encodeBytes32String("moch proof hash"); - const utxo1 = newUTXO(20, Alice); - const utxo2 = newUTXO(20, Bob); - const tx1 = await zkDvP.connect(Alice.signer).initiateTrade([utxo1.hash, 0], [utxo2.hash, 0], mockProofHash, 0, 0, mockProofHash); - const result = await tx1.wait(); - const event = zkDvP.interface.parseLog(result.logs[0]); - const tradeId = event.args.tradeId; - - const utxo3 = newAssetUTXO(25, "http://ipfs.io/file-hash-1", Bob); - await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [utxo1.hash, 0], [0, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Payment inputs already provided by the trade initiator"); - await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [0, 0], [utxo2.hash, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Payment outputs already provided by the trade initiator"); - await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [0, 0], [0, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Asset input must be provided to accept the trade"); - await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [0, 0], [0, 0], mockProofHash, utxo3.hash, 0, mockProofHash)).rejectedWith("Asset output must be provided to accept the trade"); - }); - - it("Failing cases for accepting a trade with asset terms", async function () { - const mockProofHash = encodeBytes32String("mock proof hash"); - const utxo1 = newAssetUTXO(100, "http://ipfs.io/file-hash-1", Alice); - const utxo2 = newAssetUTXO(202, "http://ipfs.io/file-hash-2", Bob); - const tx1 = await zkDvP.connect(Alice.signer).initiateTrade([0, 0], [0, 0], ZeroHash, utxo1.hash, utxo2.hash, mockProofHash); - const result = await tx1.wait(); - const event = zkDvP.interface.parseLog(result.logs[0]); - const tradeId = event.args.tradeId; - - const utxo3 = newUTXO(10, Bob); - const utxo4 = newUTXO(20, Bob); - const utxo5 = newUTXO(25, Alice); - const utxo6 = newUTXO(5, Bob); - await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [utxo3.hash, utxo4.hash], [utxo5.hash, utxo6.hash], mockProofHash, utxo1.hash, utxo2.hash, mockProofHash)).rejectedWith("Asset inputs already provided by the trade initiator"); - await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [utxo3.hash, utxo4.hash], [utxo5.hash, utxo6.hash], mockProofHash, 0, utxo2.hash, mockProofHash)).rejectedWith("Asset outputs already provided by the trade initiator"); - await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [0, 0], [0, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Payment inputs must be provided to accept the trade"); - await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [utxo3.hash, utxo4.hash], [0, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Payment outputs must be provided to accept the trade"); - }); - it("Initiating a successful DvP transaction with payment inputs and accepting by specifying asset inputs", async function () { // the authority mints some payment tokens to Alice const _utxo1 = newUTXO(100, Alice); @@ -210,5 +150,75 @@ describe("DvP flows between fungible and non-fungible tokens based on Zeto with expect(events[0].tradeId).to.equal(tradeId); expect(events[0].trade.status).to.equal(2n); // enum for TradeStatus.Completed }); + describe("failure cases", function () { + // the following failure cases rely on the hardhat network + // to return the details of the errors. This is not possible + // on non-hardhat networks + if (network.name !== 'hardhat') { + return; + } + + it("Initiating a DvP transaction without payment input or asset input should fail", async function () { + await expect(zkDvP.connect(Alice.signer).initiateTrade([0, 0], [0, 0], ZeroHash, 0, 0, ZeroHash)).rejectedWith("Payment inputs and asset input cannot be zero at the same time"); + }); + + it("Initiating a DvP transaction with payment input but no payment output should fail", async function () { + const utxo1 = newUTXO(10, Alice); + const utxo2 = newUTXO(20, Alice); + await expect(zkDvP.connect(Alice.signer).initiateTrade([utxo1.hash, utxo2.hash], [0, 0], ZeroHash, 0, 0, ZeroHash)).rejectedWith("Payment outputs cannot be zero when payment inputs are non-zero"); + }); + + it("Initiating a DvP transaction with payment inputs and asset inputs should fail", async function () { + const utxo1 = newUTXO(10, Alice); + const utxo2 = newUTXO(20, Alice); + const utxo3 = newUTXO(25, Bob); + const utxo4 = newUTXO(5, Alice); + await expect(zkDvP.connect(Alice.signer).initiateTrade([utxo1.hash, utxo2.hash], [utxo3.hash, utxo4.hash], ZeroHash, utxo3.hash, 0, ZeroHash)).rejectedWith("Payment inputs and asset input cannot be provided at the same time"); + }); + + it("Initiating a DvP transaction with asset input but no asset output should fail", async function () { + const utxo1 = newUTXO(10, Alice); + await expect(zkDvP.connect(Alice.signer).initiateTrade([0, 0], [0, 0], ZeroHash, utxo1.hash, 0, ZeroHash)).rejectedWith("Asset output cannot be zero when asset input is non-zero"); + }); + + it("Accepting a trade using an invalid trade ID should fail", async function () { + await expect(zkDvP.connect(Bob.signer).acceptTrade(1000, [0, 0], [0, 0], ZeroHash, 0, 0, ZeroHash)).rejectedWith("Trade does not exist"); + }); + + it("Failing cases for accepting a trade with payment terms", async function () { + const mockProofHash = encodeBytes32String("moch proof hash"); + const utxo1 = newUTXO(20, Alice); + const utxo2 = newUTXO(20, Bob); + const tx1 = await zkDvP.connect(Alice.signer).initiateTrade([utxo1.hash, 0], [utxo2.hash, 0], mockProofHash, 0, 0, mockProofHash); + const result = await tx1.wait(); + const event = zkDvP.interface.parseLog(result.logs[0]); + const tradeId = event.args.tradeId; + + const utxo3 = newAssetUTXO(25, "http://ipfs.io/file-hash-1", Bob); + await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [utxo1.hash, 0], [0, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Payment inputs already provided by the trade initiator"); + await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [0, 0], [utxo2.hash, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Payment outputs already provided by the trade initiator"); + await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [0, 0], [0, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Asset input must be provided to accept the trade"); + await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [0, 0], [0, 0], mockProofHash, utxo3.hash, 0, mockProofHash)).rejectedWith("Asset output must be provided to accept the trade"); + }); + + it("Failing cases for accepting a trade with asset terms", async function () { + const mockProofHash = encodeBytes32String("mock proof hash"); + const utxo1 = newAssetUTXO(100, "http://ipfs.io/file-hash-1", Alice); + const utxo2 = newAssetUTXO(202, "http://ipfs.io/file-hash-2", Bob); + const tx1 = await zkDvP.connect(Alice.signer).initiateTrade([0, 0], [0, 0], ZeroHash, utxo1.hash, utxo2.hash, mockProofHash); + const result = await tx1.wait(); + const event = zkDvP.interface.parseLog(result.logs[0]); + const tradeId = event.args.tradeId; + + const utxo3 = newUTXO(10, Bob); + const utxo4 = newUTXO(20, Bob); + const utxo5 = newUTXO(25, Alice); + const utxo6 = newUTXO(5, Bob); + await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [utxo3.hash, utxo4.hash], [utxo5.hash, utxo6.hash], mockProofHash, utxo1.hash, utxo2.hash, mockProofHash)).rejectedWith("Asset inputs already provided by the trade initiator"); + await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [utxo3.hash, utxo4.hash], [utxo5.hash, utxo6.hash], mockProofHash, 0, utxo2.hash, mockProofHash)).rejectedWith("Asset outputs already provided by the trade initiator"); + await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [0, 0], [0, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Payment inputs must be provided to accept the trade"); + await expect(zkDvP.connect(Bob.signer).acceptTrade(tradeId, [utxo3.hash, utxo4.hash], [0, 0], mockProofHash, 0, 0, mockProofHash)).rejectedWith("Payment outputs must be provided to accept the trade"); + }); + }); }).timeout(600000); diff --git a/zkp/js/integration-test/anon_enc_nullifier_non_repudiation.js b/zkp/js/integration-test/anon_enc_nullifier_non_repudiation.js index 8e7b5ac..4fdb71c 100644 --- a/zkp/js/integration-test/anon_enc_nullifier_non_repudiation.js +++ b/zkp/js/integration-test/anon_enc_nullifier_non_repudiation.js @@ -119,13 +119,13 @@ describe('main circuit tests for Zeto fungible tokens with encryption fro non-re console.log('Proving time: ', (Date.now() - startTime) / 1000, 's'); const success = await groth16.verify(verificationKey, publicSignals, proof); - console.log('nullifiers', nullifiers); - console.log('inputCommitments', inputCommitments); - console.log('outputCommitments', outputCommitments); - console.log('root', proof1.root.bigInt()); - console.log('encryptionNonce', encryptionNonce); - console.log('authorityPublicKey', Regulator.pubKey); - console.log('publicSignals', publicSignals); + // console.log('nullifiers', nullifiers); + // console.log('inputCommitments', inputCommitments); + // console.log('outputCommitments', outputCommitments); + // console.log('root', proof1.root.bigInt()); + // console.log('encryptionNonce', encryptionNonce); + // console.log('authorityPublicKey', Regulator.pubKey); + // console.log('publicSignals', publicSignals); expect(success, true); }).timeout(600000); });