Skip to content

Commit

Permalink
Merge pull request #41 from hyperledger-labs/libs
Browse files Browse the repository at this point in the history
Fix withdraw() methods in nullifier contracts for not validating the root
  • Loading branch information
jimthematrix authored Aug 21, 2024
2 parents d74d756 + bb64054 commit dfecf4d
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 178 deletions.
198 changes: 198 additions & 0 deletions go-sdk/internal/sparse-merkle-tree/storage/sql_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
1 change: 1 addition & 0 deletions solidity/contracts/zeto_anon.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
1 change: 1 addition & 0 deletions solidity/contracts/zeto_anon_enc.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
1 change: 1 addition & 0 deletions solidity/contracts/zeto_anon_enc_nullifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
1 change: 1 addition & 0 deletions solidity/contracts/zeto_anon_nullifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
1 change: 1 addition & 0 deletions solidity/contracts/zeto_anon_nullifier_kyc.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
9 changes: 9 additions & 0 deletions solidity/test/zeto_anon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(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");
});
Expand Down
9 changes: 9 additions & 0 deletions solidity/test/zeto_anon_enc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
19 changes: 19 additions & 0 deletions solidity/test/zeto_anon_enc_nullifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
19 changes: 19 additions & 0 deletions solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
18 changes: 18 additions & 0 deletions solidity/test/zeto_anon_nullifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
Loading

0 comments on commit dfecf4d

Please sign in to comment.