Skip to content

Commit dfecf4d

Browse files
authored
Merge pull request #41 from hyperledger-labs/libs
Fix withdraw() methods in nullifier contracts for not validating the root
2 parents d74d756 + bb64054 commit dfecf4d

13 files changed

+503
-178
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package storage
2+
3+
import (
4+
"math/big"
5+
"os"
6+
"testing"
7+
8+
"github.com/hyperledger-labs/zeto/go-sdk/internal/sparse-merkle-tree/node"
9+
"github.com/hyperledger-labs/zeto/go-sdk/internal/testutils"
10+
"github.com/hyperledger-labs/zeto/go-sdk/pkg/sparse-merkle-tree/core"
11+
"github.com/hyperledger-labs/zeto/go-sdk/pkg/utxo"
12+
"github.com/stretchr/testify/assert"
13+
"gorm.io/driver/sqlite"
14+
"gorm.io/gorm"
15+
)
16+
17+
type testSqlProvider struct {
18+
db *gorm.DB
19+
}
20+
21+
func (s *testSqlProvider) DB() *gorm.DB {
22+
return s.db
23+
}
24+
25+
func (s *testSqlProvider) Close() {}
26+
27+
func TestSqliteStorage(t *testing.T) {
28+
dbfile, err := os.CreateTemp("", "gorm.db")
29+
assert.NoError(t, err)
30+
defer func() {
31+
os.Remove(dbfile.Name())
32+
}()
33+
db, err := gorm.Open(sqlite.Open(dbfile.Name()), &gorm.Config{})
34+
assert.NoError(t, err)
35+
err = db.Table(core.TreeRootsTable).AutoMigrate(&core.SMTRoot{})
36+
assert.NoError(t, err)
37+
err = db.Table(core.NodesTablePrefix + "test_1").AutoMigrate(&core.SMTNode{})
38+
assert.NoError(t, err)
39+
40+
provider := &testSqlProvider{db: db}
41+
s := NewSqlStorage(provider, "test_1")
42+
assert.NoError(t, err)
43+
44+
tokenId := big.NewInt(1001)
45+
uriString := "https://example.com/token/1001"
46+
assert.NoError(t, err)
47+
sender := testutils.NewKeypair()
48+
salt1 := utxo.NewSalt()
49+
50+
utxo1 := node.NewNonFungible(tokenId, uriString, sender.PublicKey, salt1)
51+
n1, err := node.NewLeafNode(utxo1)
52+
assert.NoError(t, err)
53+
54+
idx, _ := utxo1.CalculateIndex()
55+
err = s.UpsertRootNodeIndex(idx)
56+
assert.NoError(t, err)
57+
dbIdx, err := s.GetRootNodeIndex()
58+
assert.NoError(t, err)
59+
assert.Equal(t, idx.Hex(), dbIdx.Hex())
60+
61+
dbRoot := core.SMTRoot{Name: "test_1"}
62+
err = db.Table(core.TreeRootsTable).First(&dbRoot).Error
63+
assert.NoError(t, err)
64+
assert.Equal(t, idx.Hex(), dbRoot.RootIndex)
65+
66+
err = s.InsertNode(n1)
67+
assert.NoError(t, err)
68+
69+
dbNode := core.SMTNode{RefKey: n1.Ref().Hex()}
70+
err = db.Table(core.NodesTablePrefix + "test_1").First(&dbNode).Error
71+
assert.NoError(t, err)
72+
assert.Equal(t, n1.Ref().Hex(), dbNode.RefKey)
73+
74+
n2, err := s.GetNode(n1.Ref())
75+
assert.NoError(t, err)
76+
assert.Equal(t, n1.Ref().Hex(), n2.Ref().Hex())
77+
78+
bn1, err := node.NewBranchNode(n1.Ref(), n1.Ref())
79+
assert.NoError(t, err)
80+
err = s.InsertNode(bn1)
81+
assert.NoError(t, err)
82+
83+
n3, err := s.GetNode(bn1.Ref())
84+
assert.NoError(t, err)
85+
assert.Equal(t, bn1.Ref().Hex(), n3.Ref().Hex())
86+
}
87+
88+
func TestSqliteStorageFail_NoRootTable(t *testing.T) {
89+
dbfile, err := os.CreateTemp("", "gorm.db")
90+
assert.NoError(t, err)
91+
defer func() {
92+
os.Remove(dbfile.Name())
93+
}()
94+
db, err := gorm.Open(sqlite.Open(dbfile.Name()), &gorm.Config{})
95+
assert.NoError(t, err)
96+
97+
provider := &testSqlProvider{db: db}
98+
s := NewSqlStorage(provider, "test_1")
99+
assert.NoError(t, err)
100+
101+
_, err = s.GetRootNodeIndex()
102+
assert.EqualError(t, err, "no such table: merkelTreeRoots")
103+
104+
err = db.Table(core.TreeRootsTable).AutoMigrate(&core.SMTRoot{})
105+
assert.NoError(t, err)
106+
107+
_, err = s.GetRootNodeIndex()
108+
assert.EqualError(t, err, "key not found")
109+
}
110+
111+
func TestSqliteStorageFail_NoNodeTable(t *testing.T) {
112+
dbfile, err := os.CreateTemp("", "gorm.db")
113+
assert.NoError(t, err)
114+
defer func() {
115+
os.Remove(dbfile.Name())
116+
}()
117+
db, err := gorm.Open(sqlite.Open(dbfile.Name()), &gorm.Config{})
118+
assert.NoError(t, err)
119+
120+
provider := &testSqlProvider{db: db}
121+
s := NewSqlStorage(provider, "test_1")
122+
assert.NoError(t, err)
123+
124+
idx, err := node.NewNodeIndexFromHex("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
125+
assert.NoError(t, err)
126+
_, err = s.GetNode(idx)
127+
assert.EqualError(t, err, "no such table: smtNodes_test_1")
128+
129+
err = db.Table(core.NodesTablePrefix + "test_1").AutoMigrate(&core.SMTNode{})
130+
assert.NoError(t, err)
131+
132+
_, err = s.GetNode(idx)
133+
assert.EqualError(t, err, "key not found")
134+
}
135+
136+
func TestSqliteStorageFail_BadNodeIndex(t *testing.T) {
137+
dbfile, err := os.CreateTemp("", "gorm.db")
138+
assert.NoError(t, err)
139+
defer func() {
140+
os.Remove(dbfile.Name())
141+
}()
142+
db, err := gorm.Open(sqlite.Open(dbfile.Name()), &gorm.Config{})
143+
assert.NoError(t, err)
144+
err = db.Table(core.TreeRootsTable).AutoMigrate(&core.SMTRoot{})
145+
assert.NoError(t, err)
146+
err = db.Table(core.NodesTablePrefix + "test_1").AutoMigrate(&core.SMTNode{})
147+
assert.NoError(t, err)
148+
149+
provider := &testSqlProvider{db: db}
150+
s := NewSqlStorage(provider, "test_1")
151+
assert.NoError(t, err)
152+
153+
sender := testutils.NewKeypair()
154+
salt1 := utxo.NewSalt()
155+
156+
utxo1 := node.NewFungible(big.NewInt(100), sender.PublicKey, salt1)
157+
n1, err := node.NewLeafNode(utxo1)
158+
assert.NoError(t, err)
159+
err = s.InsertNode(n1)
160+
assert.NoError(t, err)
161+
162+
// modify the index in the db
163+
dbNode := core.SMTNode{RefKey: n1.Ref().Hex()}
164+
err = db.Table(core.NodesTablePrefix + "test_1").First(&dbNode).Error
165+
assert.NoError(t, err)
166+
badIndex := ""
167+
dbNode.Index = &badIndex
168+
err = db.Table(core.NodesTablePrefix + "test_1").Save(&dbNode).Error
169+
assert.NoError(t, err)
170+
171+
_, err = s.GetNode(n1.Ref())
172+
assert.EqualError(t, err, "expected 32 bytes for the decoded node index")
173+
174+
bn1, err := node.NewBranchNode(n1.Ref(), n1.Ref())
175+
assert.NoError(t, err)
176+
err = s.InsertNode(bn1)
177+
assert.NoError(t, err)
178+
179+
dbNode = core.SMTNode{RefKey: bn1.Ref().Hex()}
180+
err = db.Table(core.NodesTablePrefix + "test_1").First(&dbNode).Error
181+
assert.NoError(t, err)
182+
saveLeftChild := *dbNode.LeftChild
183+
dbNode.LeftChild = &badIndex
184+
err = db.Table(core.NodesTablePrefix + "test_1").Save(&dbNode).Error
185+
assert.NoError(t, err)
186+
187+
_, err = s.GetNode(bn1.Ref())
188+
assert.EqualError(t, err, "expected 32 bytes for the decoded node index")
189+
190+
dbNode.LeftChild = &saveLeftChild
191+
dbNode.RightChild = &badIndex
192+
err = db.Table(core.NodesTablePrefix + "test_1").Save(&dbNode).Error
193+
assert.NoError(t, err)
194+
_, err = s.GetNode(bn1.Ref())
195+
assert.EqualError(t, err, "expected 32 bytes for the decoded node index")
196+
197+
s.Close()
198+
}

solidity/contracts/zeto_anon.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ contract Zeto_Anon is ZetoBase, ZetoFungibleWithdraw {
107107
uint256 output,
108108
Commonlib.Proof calldata proof
109109
) public {
110+
validateTransactionProposal(inputs, [output, 0], proof);
110111
_withdraw(amount, inputs, output, proof);
111112
processInputsAndOutputs(inputs, [output, 0]);
112113
}

solidity/contracts/zeto_anon_enc.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ contract Zeto_AnonEnc is ZetoBase, ZetoFungibleWithdraw {
124124
uint256 output,
125125
Commonlib.Proof calldata proof
126126
) public {
127+
validateTransactionProposal(inputs, [output, 0], proof);
127128
_withdraw(amount, inputs, output, proof);
128129
processInputsAndOutputs(inputs, [output, 0]);
129130
}

solidity/contracts/zeto_anon_enc_nullifier.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ contract Zeto_AnonEncNullifier is
140140
uint256 root,
141141
Commonlib.Proof calldata proof
142142
) public {
143+
validateTransactionProposal(nullifiers, [output, 0], root);
143144
_withdrawWithNullifiers(amount, nullifiers, output, root, proof);
144145
processInputsAndOutputs(nullifiers, [output, 0]);
145146
}

solidity/contracts/zeto_anon_enc_nullifier_non_repudiation.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ contract Zeto_AnonEncNullifierNonRepudiation is
187187
uint256 root,
188188
Commonlib.Proof calldata proof
189189
) public {
190+
validateTransactionProposal(nullifiers, [output, 0], root);
190191
_withdrawWithNullifiers(amount, nullifiers, output, root, proof);
191192
processInputsAndOutputs(nullifiers, [output, 0]);
192193
}

solidity/contracts/zeto_anon_nullifier.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ contract Zeto_AnonNullifier is
122122
uint256 root,
123123
Commonlib.Proof calldata proof
124124
) public {
125+
validateTransactionProposal(nullifiers, [output, 0], root);
125126
_withdrawWithNullifiers(amount, nullifiers, output, root, proof);
126127
processInputsAndOutputs(nullifiers, [output, 0]);
127128
}

solidity/contracts/zeto_anon_nullifier_kyc.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ contract Zeto_AnonNullifierKyc is
128128
uint256 root,
129129
Commonlib.Proof calldata proof
130130
) public {
131+
validateTransactionProposal(nullifiers, [output, 0], root);
131132
_withdrawWithNullifiers(amount, nullifiers, output, root, proof);
132133
processInputsAndOutputs(nullifiers, [output, 0]);
133134
}

solidity/test/zeto_anon.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,15 @@ describe("Zeto based fungible token with anonymity without encryption or nullifi
129129
expect(balance).to.equal(80);
130130
});
131131

132+
it("Alice attempting to withdraw spent UTXOs should fail", async function () {
133+
// Alice proposes the output ERC20 tokens
134+
const outputCommitment = newUTXO(90, Alice);
135+
136+
const { inputCommitments, outputCommitments, encodedProof } = await prepareWithdrawProof(Alice, [utxo100, ZERO_UTXO], outputCommitment);
137+
138+
await expect(zeto.connect(Alice.signer).withdraw(10, inputCommitments, outputCommitments[0], encodedProof)).rejectedWith("UTXOAlreadySpent");
139+
});
140+
132141
it("mint existing unspent UTXOs should fail", async function () {
133142
await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned");
134143
});

solidity/test/zeto_anon_enc.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ describe("Zeto based fungible token with anonymity and encryption", function ()
127127
expect(balance).to.equal(80);
128128
});
129129

130+
it("Alice attempting to withdraw spent UTXOs should fail", async function () {
131+
// Alice proposes the output ERC20 tokens
132+
const outputCommitment = newUTXO(90, Alice);
133+
134+
const { inputCommitments, outputCommitments, encodedProof } = await prepareWithdrawProof(Alice, [utxo100, ZERO_UTXO], outputCommitment);
135+
136+
await expect(zeto.connect(Alice.signer).withdraw(10, inputCommitments, outputCommitments[0], encodedProof)).rejectedWith("UTXOAlreadySpent");
137+
});
138+
130139
it("mint existing unspent UTXOs should fail", async function () {
131140
await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned");
132141
});

solidity/test/zeto_anon_enc_nullifier.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,25 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
201201
expect(balance).to.equal(80);
202202
});
203203

204+
it("Alice attempting to withdraw spent UTXOs should fail", async function () {
205+
// Alice generates the nullifiers for the UTXOs to be spent
206+
const nullifier1 = newNullifier(utxo100, Alice);
207+
208+
// Alice generates inclusion proofs for the UTXOs to be spent
209+
let root = await smtAlice.root();
210+
const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root);
211+
const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
212+
const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())];
213+
214+
// Alice proposes the output ERC20 tokens
215+
const outputCommitment = newUTXO(90, Alice);
216+
217+
const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs);
218+
219+
// Alice withdraws her UTXOs to ERC20 tokens
220+
await expect(zeto.connect(Alice.signer).withdraw(10, nullifiers, outputCommitments[0], root.bigInt(), encodedProof)).rejectedWith("UTXOAlreadySpent");
221+
});
222+
204223
it("mint existing unspent UTXOs should fail", async function () {
205224
await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned");
206225
});

solidity/test/zeto_anon_enc_nullifier_non_repudiation.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,25 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
238238
expect(balance).to.equal(80);
239239
});
240240

241+
it("Alice attempting to withdraw spent UTXOs should fail", async function () {
242+
// Alice generates the nullifiers for the UTXOs to be spent
243+
const nullifier1 = newNullifier(utxo100, Alice);
244+
245+
// Alice generates inclusion proofs for the UTXOs to be spent
246+
let root = await smtAlice.root();
247+
const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root);
248+
const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
249+
const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())];
250+
251+
// Alice proposes the output ERC20 tokens
252+
const outputCommitment = newUTXO(20, Alice);
253+
254+
const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs);
255+
256+
// Alice withdraws her UTXOs to ERC20 tokens
257+
await expect(zeto.connect(Alice.signer).withdraw(80, nullifiers, outputCommitments[0], root.bigInt(), encodedProof)).rejectedWith("UTXOAlreadySpent");
258+
});
259+
241260
it("mint existing unspent UTXOs should fail", async function () {
242261
await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned");
243262
});

solidity/test/zeto_anon_nullifier.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,24 @@ describe("Zeto based fungible token with anonymity using nullifiers without encr
202202
expect(balance).to.equal(80);
203203
});
204204

205+
it("Alice attempting to withdraw spent UTXOs should fail", async function () {
206+
// Alice generates the nullifiers for the UTXOs to be spent
207+
const nullifier1 = newNullifier(utxo100, Alice);
208+
209+
// Alice generates inclusion proofs for the UTXOs to be spent
210+
let root = await smtAlice.root();
211+
const proof1 = await smtAlice.generateCircomVerifierProof(utxo100.hash, root);
212+
const proof2 = await smtAlice.generateCircomVerifierProof(0n, root);
213+
const merkleProofs = [proof1.siblings.map((s) => s.bigInt()), proof2.siblings.map((s) => s.bigInt())];
214+
215+
// Alice proposes the output ERC20 tokens
216+
const outputCommitment = newUTXO(90, Alice);
217+
218+
const { nullifiers, outputCommitments, encodedProof } = await prepareNullifierWithdrawProof(Alice, [utxo100, ZERO_UTXO], [nullifier1, ZERO_UTXO], outputCommitment, root.bigInt(), merkleProofs);
219+
220+
await expect(zeto.connect(Alice.signer).withdraw(10, nullifiers, outputCommitments[0], root.bigInt(), encodedProof)).rejectedWith("UTXOAlreadySpent");
221+
});
222+
205223
it("mint existing unspent UTXOs should fail", async function () {
206224
await expect(doMint(zeto, deployer, [utxo4])).rejectedWith("UTXOAlreadyOwned");
207225
});

0 commit comments

Comments
 (0)