From 9776b53a7d27b2f6ba8a58e22d2c9c7daa32daa0 Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Fri, 16 Aug 2024 13:58:33 -0400 Subject: [PATCH 1/4] Fix withdraw() methods in nullifier contracts for not validating the root Signed-off-by: Jim Zhang --- .../contracts/zeto_anon_enc_nullifier.sol | 1 + ...eto_anon_enc_nullifier_non_repudiation.sol | 1 + solidity/contracts/zeto_anon_nullifier.sol | 1 + .../contracts/zeto_anon_nullifier_kyc.sol | 1 + solidity/test/zeto_anon_nullifier_kyc.ts | 386 ++++++++++-------- 5 files changed, 212 insertions(+), 178 deletions(-) diff --git a/solidity/contracts/zeto_anon_enc_nullifier.sol b/solidity/contracts/zeto_anon_enc_nullifier.sol index 3025a63..5a2d4cf 100644 --- a/solidity/contracts/zeto_anon_enc_nullifier.sol +++ b/solidity/contracts/zeto_anon_enc_nullifier.sol @@ -140,6 +140,7 @@ contract Zeto_AnonEncNullifier is uint256 root, Commonlib.Proof calldata proof ) public { + validateTransactionProposal(nullifiers, [output, 0], root); _withdrawWithNullifiers(amount, nullifiers, output, root, proof); processInputsAndOutputs(nullifiers, [output, 0]); } diff --git a/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol b/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol index feb9c97..6061d7d 100644 --- a/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol +++ b/solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol @@ -187,6 +187,7 @@ contract Zeto_AnonEncNullifierNonRepudiation is uint256 root, Commonlib.Proof calldata proof ) public { + validateTransactionProposal(nullifiers, [output, 0], root); _withdrawWithNullifiers(amount, nullifiers, output, root, proof); processInputsAndOutputs(nullifiers, [output, 0]); } diff --git a/solidity/contracts/zeto_anon_nullifier.sol b/solidity/contracts/zeto_anon_nullifier.sol index 948836f..10e9c16 100644 --- a/solidity/contracts/zeto_anon_nullifier.sol +++ b/solidity/contracts/zeto_anon_nullifier.sol @@ -122,6 +122,7 @@ contract Zeto_AnonNullifier is uint256 root, Commonlib.Proof calldata proof ) public { + validateTransactionProposal(nullifiers, [output, 0], root); _withdrawWithNullifiers(amount, nullifiers, output, root, proof); processInputsAndOutputs(nullifiers, [output, 0]); } diff --git a/solidity/contracts/zeto_anon_nullifier_kyc.sol b/solidity/contracts/zeto_anon_nullifier_kyc.sol index 98c0e10..23a8540 100644 --- a/solidity/contracts/zeto_anon_nullifier_kyc.sol +++ b/solidity/contracts/zeto_anon_nullifier_kyc.sol @@ -128,6 +128,7 @@ contract Zeto_AnonNullifierKyc is uint256 root, Commonlib.Proof calldata proof ) public { + validateTransactionProposal(nullifiers, [output, 0], root); _withdrawWithNullifiers(amount, nullifiers, output, root, proof); processInputsAndOutputs(nullifiers, [output, 0]); } diff --git a/solidity/test/zeto_anon_nullifier_kyc.ts b/solidity/test/zeto_anon_nullifier_kyc.ts index 448f1b2..ca9a444 100644 --- a/solidity/test/zeto_anon_nullifier_kyc.ts +++ b/solidity/test/zeto_anon_nullifier_kyc.ts @@ -34,12 +34,14 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou let erc20: any; let zeto: any; let utxo100: UTXO; - let unregisteredUtxo100: UTXO; let utxo1: UTXO; let utxo2: UTXO; let utxo3: UTXO; + let _utxo3: UTXO; let utxo4: UTXO; + let utxo6: UTXO; let utxo7: UTXO; + let withdrawUTXO: UTXO; let circuit: any, provingKey: any; let smtAlice: Merkletree; let smtBob: Merkletree; @@ -67,13 +69,13 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou circuit = await loadCircuit('anon_nullifier_kyc'); ({ provingKeyFile: provingKey } = loadProvingKeys('anon_nullifier_kyc')); - const storage1 = new InMemoryDB(str2Bytes("")) + const storage1 = new InMemoryDB(str2Bytes("alice")) smtAlice = new Merkletree(storage1, true, 64); - const storage2 = new InMemoryDB(str2Bytes("")) + const storage2 = new InMemoryDB(str2Bytes("bob")) smtBob = new Merkletree(storage2, true, 64); - const storage3 = new InMemoryDB(str2Bytes("")) + const storage3 = new InMemoryDB(str2Bytes("kyc")) smtKyc = new Merkletree(storage3, true, 10); const publicKey1 = parseRegistryEvents(zeto, result1); @@ -129,7 +131,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou await smtBob.add(_utxo2, _utxo2); // Alice proposes the output UTXOs for the transfer to Bob - const _utxo3 = newUTXO(25, Bob); + _utxo3 = newUTXO(25, Bob); utxo4 = newUTXO(5, Alice); // Alice generates the nullifiers for the UTXOs to be spent @@ -193,7 +195,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou const utxosMerkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; // Bob proposes the output UTXOs - const utxo6 = newUTXO(10, Charlie); + utxo6 = newUTXO(10, Charlie); utxo7 = newUTXO(15, Bob); // Bob generates inclusion proofs for the identities in the transaction @@ -219,6 +221,7 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou const events = parseUTXOEvents(zeto, result.txResult!); await smtAlice.add(events[0].outputs[0], events[0].outputs[0]); await smtAlice.add(events[0].outputs[1], events[0].outputs[1]); + }).timeout(600000); it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () { @@ -231,10 +234,9 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou 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 proposes the output UTXO as remainder of the withdrawal + withdrawUTXO = newUTXO(20, Alice); + const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], withdrawUTXO, root.bigInt(), merkleProofs); // Alice withdraws her UTXOs to ERC20 tokens const tx = await zeto.connect(Alice.signer).withdraw(80, nullifiers, outputCommitments[0], root.bigInt(), encodedProof); @@ -245,178 +247,206 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou expect(balance).to.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("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())]; - - const identitiesRoot = await smtKyc.root(); - const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(Alice.babyJubPublicKey), identitiesRoot); - const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); - const identitiesMerkleProofs = [ - proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Alice) - proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) - proof3.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (Alice) - ]; - - await expect(doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [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())]; - - const identitiesRoot = await smtKyc.root(); - const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); - const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Alice.babyJubPublicKey), identitiesRoot); - const identitiesMerkleProofs = [ - proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob) - proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice) - proof4.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (Alice) - ]; - - await expect(doTransfer(Bob, [utxo7, _utxo1], [nullifier1, nullifier2], [utxo1, utxo2], root.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [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())]; - - const identitiesRoot = await smtKyc.root(); - const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); - const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Alice.babyJubPublicKey), identitiesRoot); - const identitiesMerkleProofs = [ - proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob) - proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice) - proof3.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (Bob) - ]; - - await expect(doTransfer(Bob, [utxo7, utxo7], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [Alice, Bob])).rejectedWith(`UTXODuplicate`); - }).timeout(600000); - it("transfer from an unregistered user should fail", async function () { - const tx = await erc20.connect(deployer).mint(unregistered.ethAddress, 100); - await tx.wait(); - const tx1 = await erc20.connect(unregistered.signer).approve(zeto.target, 100); - await tx1.wait(); - unregisteredUtxo100 = newUTXO(100, unregistered); - const { outputCommitments, encodedProof } = await prepareDepositProof(unregistered, unregisteredUtxo100); - const tx2 = await zeto.connect(unregistered.signer).deposit(100, outputCommitments[0], encodedProof); - await tx2.wait(); - - // for convenience we use Alice's SMT to generate the UTXO proof - await smtAlice.add(unregisteredUtxo100.hash, unregisteredUtxo100.hash); - const nullifier = newNullifier(unregisteredUtxo100, unregistered); - const output1 = newUTXO(100, Bob); - const output2 = newUTXO(0, unregistered); - const proof = await smtAlice.generateCircomVerifierProof(unregisteredUtxo100.hash, await smtAlice.root()); - const merkleProofs = [proof.siblings.map((s) => s.bigInt()), proof.siblings.map((s) => s.bigInt())]; - - // add the unregistered user to the local KYC SMT, but not to the onchain SMT - await smtKyc.add(kycHash(unregistered.babyJubPublicKey), kycHash(unregistered.babyJubPublicKey)); - const identitiesRoot = await smtKyc.root(); - const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(unregistered.babyJubPublicKey), identitiesRoot); - const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); - const identitiesMerkleProofs = [ - proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (unregistered) - proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) - proof3.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (unregistered) - ]; - await expect(doTransfer(unregistered, [unregisteredUtxo100, ZERO_UTXO], [nullifier, ZERO_UTXO], [output1, output2], (await smtAlice.root()).bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [Bob, unregistered])).rejectedWith("UTXORootNotFound"); + 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).mint(unregistered.ethAddress, 100); + await tx.wait(); + const tx1 = await erc20.connect(unregistered.signer).approve(zeto.target, 100); + await tx1.wait(); + + unregisteredUtxo100 = newUTXO(100, unregistered); + const { outputCommitments, encodedProof } = await prepareDepositProof(unregistered, unregisteredUtxo100); + const tx2 = await zeto.connect(unregistered.signer).deposit(100, outputCommitments[0], encodedProof); + await tx2.wait(); + }); + + it("transfer from an unregistered user should fail", async function () { + // catch up the local SMT for the unregistered user + await smtUnregistered.add(utxo100.hash, utxo100.hash); + await smtUnregistered.add(utxo1.hash, utxo1.hash); + await smtUnregistered.add(utxo2.hash, utxo2.hash); + await smtUnregistered.add(_utxo3.hash, _utxo3.hash); + await smtUnregistered.add(utxo4.hash, utxo4.hash); + await smtUnregistered.add(utxo6.hash, utxo6.hash); + await smtUnregistered.add(utxo7.hash, utxo7.hash); + await smtUnregistered.add(withdrawUTXO.hash, withdrawUTXO.hash); + await smtUnregistered.add(unregisteredUtxo100.hash, unregisteredUtxo100.hash); + const utxosRoot = await smtUnregistered.root(); + + const nullifier = newNullifier(unregisteredUtxo100, unregistered); + const output1 = newUTXO(100, Bob); + const output2 = newUTXO(0, unregistered); + const proof = await smtUnregistered.generateCircomVerifierProof(unregisteredUtxo100.hash, utxosRoot); + const merkleProofs = [proof.siblings.map((s) => s.bigInt()), proof.siblings.map((s) => s.bigInt())]; + + // add the unregistered user to the local KYC SMT, but not to the onchain SMT + await smtKyc.add(kycHash(unregistered.babyJubPublicKey), kycHash(unregistered.babyJubPublicKey)); + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(unregistered.babyJubPublicKey), identitiesRoot); + const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (unregistered) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) + proof3.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (unregistered) + ]; + await expect(doTransfer(unregistered, [unregisteredUtxo100, ZERO_UTXO], [nullifier, ZERO_UTXO], [output1, output2], utxosRoot.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [Bob, unregistered])).rejectedWith("Invalid proof"); + }); + + it("the unregistered user can still withdraw their UTXOs to ERC20 tokens", async function () { + // unregistered user generates the nullifiers for the UTXOs to be spent + const nullifier1 = newNullifier(unregisteredUtxo100, unregistered); + + // unregistered user generates inclusion proofs for the UTXOs to be spent + let root = await smtUnregistered.root(); + const proof1 = await smtUnregistered.generateCircomVerifierProof(unregisteredUtxo100.hash, root); + const proof2 = await smtUnregistered.generateCircomVerifierProof(0n, root); + const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; + + // unregistered user proposes the output ERC20 tokens + const outputCommitment = newUTXO(0, unregistered); + + const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(unregistered, [unregisteredUtxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs); + + // unregistered user withdraws her UTXOs to ERC20 tokens + const tx = await zeto.connect(unregistered.signer).withdraw(100, nullifiers, outputCommitments[0], root.bigInt(), encodedProof); + await tx.wait(); + + // unregistered user checks her ERC20 balance + const balance = await erc20.balanceOf(unregistered.ethAddress); + expect(balance).to.equal(100); + }); }); - it("the unregistered user can still withdraw their UTXOs to ERC20 tokens", async function () { - // unregistered user generates the nullifiers for the UTXOs to be spent - const nullifier1 = newNullifier(unregisteredUtxo100, unregistered); - - // unregistered user generates inclusion proofs for the UTXOs to be spent - let root = await smtAlice.root(); - const proof1 = await smtAlice.generateCircomVerifierProof(unregisteredUtxo100.hash, root); - const proof2 = await smtAlice.generateCircomVerifierProof(0n, root); - const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())]; - - // unregistered user proposes the output ERC20 tokens - const outputCommitment = newUTXO(0, unregistered); - - const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(unregistered, [unregisteredUtxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs); - - // unregistered user withdraws her UTXOs to ERC20 tokens - const tx = await zeto.connect(unregistered.signer).withdraw(100, nullifiers, outputCommitments[0], root.bigInt(), encodedProof); - await tx.wait(); - - // unregistered user checks her ERC20 balance - const balance = await erc20.balanceOf(unregistered.ethAddress); - expect(balance).to.equal(100); + describe("failure flows", function () { + + 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())]; + + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(Alice.babyJubPublicKey), identitiesRoot); + const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Alice) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) + proof3.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (Alice) + ]; + + await expect(doTransfer(Alice, [utxo1, utxo2], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [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())]; + + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); + const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Alice.babyJubPublicKey), identitiesRoot); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice) + proof4.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (Alice) + ]; + + await expect(doTransfer(Bob, [utxo7, _utxo1], [nullifier1, nullifier2], [utxo1, utxo2], root.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [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())]; + + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); + const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Alice.babyJubPublicKey), identitiesRoot); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Bob) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Alice) + proof3.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (Bob) + ]; + + await expect(doTransfer(Bob, [utxo7, utxo7], [nullifier1, nullifier2], [_utxo1, _utxo2], root.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [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); + const utxo7 = newUTXO(15, Bob); + + const identitiesRoot = await smtKyc.root(); + const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(Alice.babyJubPublicKey), identitiesRoot); + const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); + const proof5 = await smtKyc.generateCircomVerifierProof(kycHash(Charlie.babyJubPublicKey), identitiesRoot); + const identitiesMerkleProofs = [ + proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Alice) + proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) + proof5.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (Charlie) + ]; + + await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); + }).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); - - const identitiesRoot = await smtKyc.root(); - const proof3 = await smtKyc.generateCircomVerifierProof(kycHash(Alice.babyJubPublicKey), identitiesRoot); - const proof4 = await smtKyc.generateCircomVerifierProof(kycHash(Bob.babyJubPublicKey), identitiesRoot); - const proof5 = await smtKyc.generateCircomVerifierProof(kycHash(Charlie.babyJubPublicKey), identitiesRoot); - const identitiesMerkleProofs = [ - proof3.siblings.map((s) => s.bigInt()), // identity proof for the sender (Alice) - proof4.siblings.map((s) => s.bigInt()), // identity proof for the 1st owner of the output UTXO (Bob) - proof5.siblings.map((s) => s.bigInt()) // identity proof for the 2nd owner of the output UTXO (Charlie) - ]; - - await expect(doTransfer(Alice, [nonExisting1, nonExisting2], [nullifier1, nullifier2], [utxo7, _utxo1], root.bigInt(), merkleProofs, identitiesRoot.bigInt(), identitiesMerkleProofs, [Bob, Charlie])).rejectedWith("UTXORootNotFound"); - }).timeout(600000); - async function doTransfer(signer: User, inputs: UTXO[], _nullifiers: UTXO[], outputs: UTXO[], utxosRoot: BigInt, utxosMerkleProofs: BigInt[][], identitiesRoot: BigInt, identitiesMerkleProof: BigInt[][], owners: User[]) { let nullifiers: [BigNumberish, BigNumberish]; let outputCommitments: [BigNumberish, BigNumberish]; From 5985fe94f2a3c4b0a744675698b9241c210d00d5 Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Tue, 20 Aug 2024 15:17:07 -0400 Subject: [PATCH 2/4] Add validation to withdraw() in anon and anon_enc tokens Signed-off-by: Jim Zhang --- solidity/contracts/zeto_anon.sol | 1 + solidity/contracts/zeto_anon_enc.sol | 1 + solidity/test/zeto_anon.ts | 9 +++++++++ solidity/test/zeto_anon_enc.ts | 9 +++++++++ 4 files changed, 20 insertions(+) diff --git a/solidity/contracts/zeto_anon.sol b/solidity/contracts/zeto_anon.sol index 4761957..54fe56c 100644 --- a/solidity/contracts/zeto_anon.sol +++ b/solidity/contracts/zeto_anon.sol @@ -107,6 +107,7 @@ contract Zeto_Anon is ZetoBase, ZetoFungibleWithdraw { uint256 output, Commonlib.Proof calldata proof ) public { + validateTransactionProposal(inputs, [output, 0], proof); _withdraw(amount, inputs, output, proof); processInputsAndOutputs(inputs, [output, 0]); } diff --git a/solidity/contracts/zeto_anon_enc.sol b/solidity/contracts/zeto_anon_enc.sol index 1d59ef8..a507c24 100644 --- a/solidity/contracts/zeto_anon_enc.sol +++ b/solidity/contracts/zeto_anon_enc.sol @@ -124,6 +124,7 @@ contract Zeto_AnonEnc is ZetoBase, ZetoFungibleWithdraw { uint256 output, Commonlib.Proof calldata proof ) public { + validateTransactionProposal(inputs, [output, 0], proof); _withdraw(amount, inputs, output, proof); processInputsAndOutputs(inputs, [output, 0]); } diff --git a/solidity/test/zeto_anon.ts b/solidity/test/zeto_anon.ts index a1bce04..f1bba4e 100644 --- a/solidity/test/zeto_anon.ts +++ b/solidity/test/zeto_anon.ts @@ -129,6 +129,15 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi 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(20, 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"); }); diff --git a/solidity/test/zeto_anon_enc.ts b/solidity/test/zeto_anon_enc.ts index 75f5d2d..f488ff7 100644 --- a/solidity/test/zeto_anon_enc.ts +++ b/solidity/test/zeto_anon_enc.ts @@ -127,6 +127,15 @@ describe("Zeto based fungible token with anonymity and encryption", function () 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"); + }); + it("mint existing unspent UTXOs should fail", async function () { await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned"); }); From 39d7ede74281dfb0ca600615e28cedcf1fea5d4f Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Tue, 20 Aug 2024 15:36:23 -0400 Subject: [PATCH 3/4] Add tests for the withdraw of spent UTXOs failure cases Signed-off-by: Jim Zhang --- solidity/test/zeto_anon.ts | 2 +- solidity/test/zeto_anon_enc_nullifier.ts | 19 +++++++++++++++++++ ...zeto_anon_enc_nullifier_non_repudiation.ts | 19 +++++++++++++++++++ solidity/test/zeto_anon_nullifier.ts | 18 ++++++++++++++++++ solidity/test/zeto_anon_nullifier_kyc.ts | 17 +++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/solidity/test/zeto_anon.ts b/solidity/test/zeto_anon.ts index f1bba4e..b8413f5 100644 --- a/solidity/test/zeto_anon.ts +++ b/solidity/test/zeto_anon.ts @@ -131,7 +131,7 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi it("Alice attempting to withdraw spent UTXOs should fail", async function () { // Alice proposes the output ERC20 tokens - const outputCommitment = newUTXO(20, Alice); + const outputCommitment = newUTXO(90, Alice); const { inputCommitments, outputCommitments, encodedProof } = await prepareWithdrawProof(Alice, [utxo100, ZERO_UTXO], outputCommitment); diff --git a/solidity/test/zeto_anon_enc_nullifier.ts b/solidity/test/zeto_anon_enc_nullifier.ts index 9364f3d..ec84e04 100644 --- a/solidity/test/zeto_anon_enc_nullifier.ts +++ b/solidity/test/zeto_anon_enc_nullifier.ts @@ -201,6 +201,25 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti expect(balance).to.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"); }); diff --git a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts index 12ca851..77ede3c 100644 --- a/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts +++ b/solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts @@ -238,6 +238,25 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti expect(balance).to.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"); }); diff --git a/solidity/test/zeto_anon_nullifier.ts b/solidity/test/zeto_anon_nullifier.ts index 95fd9ea..14592ce 100644 --- a/solidity/test/zeto_anon_nullifier.ts +++ b/solidity/test/zeto_anon_nullifier.ts @@ -202,6 +202,24 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr expect(balance).to.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"); }); diff --git a/solidity/test/zeto_anon_nullifier_kyc.ts b/solidity/test/zeto_anon_nullifier_kyc.ts index ca9a444..44d788f 100644 --- a/solidity/test/zeto_anon_nullifier_kyc.ts +++ b/solidity/test/zeto_anon_nullifier_kyc.ts @@ -328,6 +328,23 @@ describe("Zeto based fungible token with anonymity, KYC, using nullifiers withou describe("failure flows", function () { + 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 UTXO as remainder of the withdrawal + withdrawUTXO = newUTXO(90, Alice); + const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], withdrawUTXO, 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"); }); From bb64054f08500ce88f9f1479bc1a164528f6ba09 Mon Sep 17 00:00:00 2001 From: Jim Zhang Date: Tue, 20 Aug 2024 16:28:31 -0400 Subject: [PATCH 4/4] Add tests for the storage sql package Signed-off-by: Jim Zhang --- .../sparse-merkle-tree/storage/sql_test.go | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 go-sdk/internal/sparse-merkle-tree/storage/sql_test.go diff --git a/go-sdk/internal/sparse-merkle-tree/storage/sql_test.go b/go-sdk/internal/sparse-merkle-tree/storage/sql_test.go new file mode 100644 index 0000000..f7f00a6 --- /dev/null +++ b/go-sdk/internal/sparse-merkle-tree/storage/sql_test.go @@ -0,0 +1,198 @@ +package storage + +import ( + "math/big" + "os" + "testing" + + "github.com/hyperledger-labs/zeto/go-sdk/internal/sparse-merkle-tree/node" + "github.com/hyperledger-labs/zeto/go-sdk/internal/testutils" + "github.com/hyperledger-labs/zeto/go-sdk/pkg/sparse-merkle-tree/core" + "github.com/hyperledger-labs/zeto/go-sdk/pkg/utxo" + "github.com/stretchr/testify/assert" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type testSqlProvider struct { + db *gorm.DB +} + +func (s *testSqlProvider) DB() *gorm.DB { + return s.db +} + +func (s *testSqlProvider) Close() {} + +func TestSqliteStorage(t *testing.T) { + dbfile, err := os.CreateTemp("", "gorm.db") + assert.NoError(t, err) + defer func() { + os.Remove(dbfile.Name()) + }() + db, err := gorm.Open(sqlite.Open(dbfile.Name()), &gorm.Config{}) + assert.NoError(t, err) + err = db.Table(core.TreeRootsTable).AutoMigrate(&core.SMTRoot{}) + assert.NoError(t, err) + err = db.Table(core.NodesTablePrefix + "test_1").AutoMigrate(&core.SMTNode{}) + assert.NoError(t, err) + + provider := &testSqlProvider{db: db} + s := NewSqlStorage(provider, "test_1") + assert.NoError(t, err) + + tokenId := big.NewInt(1001) + uriString := "https://example.com/token/1001" + assert.NoError(t, err) + sender := testutils.NewKeypair() + salt1 := utxo.NewSalt() + + utxo1 := node.NewNonFungible(tokenId, uriString, sender.PublicKey, salt1) + n1, err := node.NewLeafNode(utxo1) + assert.NoError(t, err) + + idx, _ := utxo1.CalculateIndex() + err = s.UpsertRootNodeIndex(idx) + assert.NoError(t, err) + dbIdx, err := s.GetRootNodeIndex() + assert.NoError(t, err) + assert.Equal(t, idx.Hex(), dbIdx.Hex()) + + dbRoot := core.SMTRoot{Name: "test_1"} + err = db.Table(core.TreeRootsTable).First(&dbRoot).Error + assert.NoError(t, err) + assert.Equal(t, idx.Hex(), dbRoot.RootIndex) + + err = s.InsertNode(n1) + assert.NoError(t, err) + + dbNode := core.SMTNode{RefKey: n1.Ref().Hex()} + err = db.Table(core.NodesTablePrefix + "test_1").First(&dbNode).Error + assert.NoError(t, err) + assert.Equal(t, n1.Ref().Hex(), dbNode.RefKey) + + n2, err := s.GetNode(n1.Ref()) + assert.NoError(t, err) + assert.Equal(t, n1.Ref().Hex(), n2.Ref().Hex()) + + bn1, err := node.NewBranchNode(n1.Ref(), n1.Ref()) + assert.NoError(t, err) + err = s.InsertNode(bn1) + assert.NoError(t, err) + + n3, err := s.GetNode(bn1.Ref()) + assert.NoError(t, err) + assert.Equal(t, bn1.Ref().Hex(), n3.Ref().Hex()) +} + +func TestSqliteStorageFail_NoRootTable(t *testing.T) { + dbfile, err := os.CreateTemp("", "gorm.db") + assert.NoError(t, err) + defer func() { + os.Remove(dbfile.Name()) + }() + db, err := gorm.Open(sqlite.Open(dbfile.Name()), &gorm.Config{}) + assert.NoError(t, err) + + provider := &testSqlProvider{db: db} + s := NewSqlStorage(provider, "test_1") + assert.NoError(t, err) + + _, err = s.GetRootNodeIndex() + assert.EqualError(t, err, "no such table: merkelTreeRoots") + + err = db.Table(core.TreeRootsTable).AutoMigrate(&core.SMTRoot{}) + assert.NoError(t, err) + + _, err = s.GetRootNodeIndex() + assert.EqualError(t, err, "key not found") +} + +func TestSqliteStorageFail_NoNodeTable(t *testing.T) { + dbfile, err := os.CreateTemp("", "gorm.db") + assert.NoError(t, err) + defer func() { + os.Remove(dbfile.Name()) + }() + db, err := gorm.Open(sqlite.Open(dbfile.Name()), &gorm.Config{}) + assert.NoError(t, err) + + provider := &testSqlProvider{db: db} + s := NewSqlStorage(provider, "test_1") + assert.NoError(t, err) + + idx, err := node.NewNodeIndexFromHex("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + assert.NoError(t, err) + _, err = s.GetNode(idx) + assert.EqualError(t, err, "no such table: smtNodes_test_1") + + err = db.Table(core.NodesTablePrefix + "test_1").AutoMigrate(&core.SMTNode{}) + assert.NoError(t, err) + + _, err = s.GetNode(idx) + assert.EqualError(t, err, "key not found") +} + +func TestSqliteStorageFail_BadNodeIndex(t *testing.T) { + dbfile, err := os.CreateTemp("", "gorm.db") + assert.NoError(t, err) + defer func() { + os.Remove(dbfile.Name()) + }() + db, err := gorm.Open(sqlite.Open(dbfile.Name()), &gorm.Config{}) + assert.NoError(t, err) + err = db.Table(core.TreeRootsTable).AutoMigrate(&core.SMTRoot{}) + assert.NoError(t, err) + err = db.Table(core.NodesTablePrefix + "test_1").AutoMigrate(&core.SMTNode{}) + assert.NoError(t, err) + + provider := &testSqlProvider{db: db} + s := NewSqlStorage(provider, "test_1") + assert.NoError(t, err) + + sender := testutils.NewKeypair() + salt1 := utxo.NewSalt() + + utxo1 := node.NewFungible(big.NewInt(100), sender.PublicKey, salt1) + n1, err := node.NewLeafNode(utxo1) + assert.NoError(t, err) + err = s.InsertNode(n1) + assert.NoError(t, err) + + // modify the index in the db + dbNode := core.SMTNode{RefKey: n1.Ref().Hex()} + err = db.Table(core.NodesTablePrefix + "test_1").First(&dbNode).Error + assert.NoError(t, err) + badIndex := "" + dbNode.Index = &badIndex + err = db.Table(core.NodesTablePrefix + "test_1").Save(&dbNode).Error + assert.NoError(t, err) + + _, err = s.GetNode(n1.Ref()) + assert.EqualError(t, err, "expected 32 bytes for the decoded node index") + + bn1, err := node.NewBranchNode(n1.Ref(), n1.Ref()) + assert.NoError(t, err) + err = s.InsertNode(bn1) + assert.NoError(t, err) + + dbNode = core.SMTNode{RefKey: bn1.Ref().Hex()} + err = db.Table(core.NodesTablePrefix + "test_1").First(&dbNode).Error + assert.NoError(t, err) + saveLeftChild := *dbNode.LeftChild + dbNode.LeftChild = &badIndex + err = db.Table(core.NodesTablePrefix + "test_1").Save(&dbNode).Error + assert.NoError(t, err) + + _, err = s.GetNode(bn1.Ref()) + assert.EqualError(t, err, "expected 32 bytes for the decoded node index") + + dbNode.LeftChild = &saveLeftChild + dbNode.RightChild = &badIndex + err = db.Table(core.NodesTablePrefix + "test_1").Save(&dbNode).Error + assert.NoError(t, err) + _, err = s.GetNode(bn1.Ref()) + assert.EqualError(t, err, "expected 32 bytes for the decoded node index") + + s.Close() +}