@lfdecentralizedtrust/zeto-contracts 0.2.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +70 -162
  2. package/contracts/factory.sol +30 -34
  3. package/contracts/factory_upgradeable.sol +11 -7
  4. package/contracts/lib/common/util.sol +40 -0
  5. package/contracts/lib/interfaces/ILockableCapability.sol +318 -0
  6. package/contracts/lib/interfaces/{izeto.sol → IZeto.sol} +13 -1
  7. package/contracts/lib/interfaces/{izeto_initializable.sol → IZetoInitializable.sol} +14 -12
  8. package/contracts/lib/interfaces/{izeto_kyc.sol → IZetoKyc.sol} +3 -3
  9. package/contracts/lib/interfaces/IZetoLockableCapability.sol +237 -0
  10. package/contracts/lib/interfaces/IZetoStorage.sol +106 -0
  11. package/contracts/lib/interfaces/{izeto_lockable.sol → IZetoVerifier.sol} +8 -21
  12. package/contracts/lib/registry.sol +74 -26
  13. package/contracts/lib/storage/base.sol +210 -0
  14. package/contracts/lib/storage/nullifier.sol +166 -0
  15. package/contracts/lib/zeto_common.sol +277 -28
  16. package/contracts/lib/zeto_fungible.sol +451 -33
  17. package/contracts/lib/zeto_fungible_base.sol +69 -0
  18. package/contracts/lib/zeto_fungible_burn.sol +83 -53
  19. package/contracts/lib/zeto_fungible_burn_nullifier.sol +115 -72
  20. package/contracts/lib/zeto_fungible_nullifier.sol +167 -0
  21. package/contracts/lib/zeto_lockable.sol +613 -0
  22. package/contracts/lib/zeto_non_fungible.sol +236 -0
  23. package/contracts/lib/zeto_non_fungible_base.sol +61 -0
  24. package/contracts/lib/zeto_non_fungible_nullifier.sol +61 -0
  25. package/contracts/test/escrow1.sol +90 -34
  26. package/contracts/test/escrow2.sol +64 -29
  27. package/contracts/test/qurrency.sol +10 -2
  28. package/contracts/test/smt.sol +6 -1
  29. package/contracts/test/tendecimals.sol +14 -12
  30. package/contracts/verifiers/impl/anon.sol +189 -0
  31. package/contracts/verifiers/impl/anon_batch.sol +301 -0
  32. package/contracts/verifiers/impl/anon_enc.sol +266 -0
  33. package/contracts/verifiers/impl/anon_enc_batch.sol +602 -0
  34. package/contracts/verifiers/impl/anon_enc_nullifier.sol +287 -0
  35. package/contracts/verifiers/impl/anon_enc_nullifier_batch.sol +679 -0
  36. package/contracts/verifiers/impl/anon_enc_nullifier_kyc.sol +294 -0
  37. package/contracts/verifiers/impl/anon_enc_nullifier_kyc_batch.sol +686 -0
  38. package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation.sol +413 -0
  39. package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation_batch.sol +1141 -0
  40. package/contracts/verifiers/impl/anon_nullifier_kyc_transfer.sol +217 -0
  41. package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked.sol +196 -0
  42. package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked_batch.sol +308 -0
  43. package/contracts/verifiers/impl/anon_nullifier_kyc_transfer_batch.sol +385 -0
  44. package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer.sol +497 -0
  45. package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer_batch.sol +1001 -0
  46. package/contracts/verifiers/{verifier_anon_nullifier_transferLocked.sol → impl/anon_nullifier_transfer.sol} +12 -19
  47. package/contracts/verifiers/{verifier_anon_nullifier_transferLocked_batch.sol → impl/anon_nullifier_transfer_batch.sol} +44 -51
  48. package/contracts/verifiers/impl/burn.sol +182 -0
  49. package/contracts/verifiers/impl/burn_batch.sol +238 -0
  50. package/contracts/verifiers/impl/burn_nullifier.sol +203 -0
  51. package/contracts/verifiers/impl/burn_nullifier_batch.sol +315 -0
  52. package/contracts/verifiers/impl/deposit.sol +182 -0
  53. package/contracts/verifiers/impl/deposit_kyc.sol +189 -0
  54. package/contracts/verifiers/impl/nf_anon.sol +175 -0
  55. package/contracts/verifiers/{verifier_nf_anon_nullifier_transferLocked.sol → impl/nf_anon_nullifier_transfer.sol} +6 -13
  56. package/contracts/verifiers/impl/withdraw.sol +189 -0
  57. package/contracts/verifiers/impl/withdraw_batch.sol +245 -0
  58. package/contracts/verifiers/impl/withdraw_nullifier.sol +210 -0
  59. package/contracts/verifiers/impl/withdraw_nullifier_batch.sol +322 -0
  60. package/contracts/verifiers/verifier_anon.sol +31 -186
  61. package/contracts/verifiers/verifier_anon_batch.sol +31 -298
  62. package/contracts/verifiers/verifier_anon_enc.sol +31 -263
  63. package/contracts/verifiers/verifier_anon_enc_batch.sol +31 -599
  64. package/contracts/verifiers/verifier_anon_enc_nullifier.sol +31 -284
  65. package/contracts/verifiers/verifier_anon_enc_nullifier_batch.sol +33 -676
  66. package/contracts/verifiers/verifier_anon_enc_nullifier_kyc.sol +31 -291
  67. package/contracts/verifiers/verifier_anon_enc_nullifier_kyc_batch.sol +33 -683
  68. package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation.sol +33 -410
  69. package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation_batch.sol +33 -1138
  70. package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer.sol +33 -214
  71. package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked.sol +33 -221
  72. package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked_batch.sol +33 -389
  73. package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer_batch.sol +33 -382
  74. package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer.sol +33 -221
  75. package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer_batch.sol +33 -389
  76. package/contracts/verifiers/verifier_anon_nullifier_transfer.sol +33 -207
  77. package/contracts/verifiers/verifier_anon_nullifier_transfer_batch.sol +33 -375
  78. package/contracts/verifiers/verifier_burn.sol +31 -179
  79. package/contracts/verifiers/verifier_burn_batch.sol +31 -235
  80. package/contracts/verifiers/verifier_burn_nullifier.sol +31 -200
  81. package/contracts/verifiers/verifier_burn_nullifier_batch.sol +31 -312
  82. package/contracts/verifiers/verifier_deposit.sol +31 -179
  83. package/contracts/verifiers/verifier_deposit_kyc.sol +34 -0
  84. package/contracts/verifiers/verifier_nf_anon.sol +31 -172
  85. package/contracts/verifiers/verifier_nf_anon_nullifier_transfer.sol +33 -179
  86. package/contracts/verifiers/verifier_withdraw.sol +31 -186
  87. package/contracts/verifiers/verifier_withdraw_batch.sol +31 -242
  88. package/contracts/verifiers/verifier_withdraw_nullifier.sol +31 -207
  89. package/contracts/verifiers/verifier_withdraw_nullifier_batch.sol +33 -319
  90. package/contracts/zeto_anon.sol +77 -231
  91. package/contracts/zeto_anon_burnable.sol +56 -12
  92. package/contracts/zeto_anon_enc.sol +93 -190
  93. package/contracts/zeto_anon_enc_nullifier.sol +249 -195
  94. package/contracts/zeto_anon_enc_nullifier_kyc.sol +51 -232
  95. package/contracts/zeto_anon_enc_nullifier_non_repudiation.sol +231 -238
  96. package/contracts/zeto_anon_nullifier.sol +164 -298
  97. package/contracts/zeto_anon_nullifier_burnable.sol +68 -18
  98. package/contracts/zeto_anon_nullifier_kyc.sol +40 -345
  99. package/contracts/zeto_anon_nullifier_qurrency.sol +217 -361
  100. package/contracts/zeto_nf_anon.sol +55 -130
  101. package/contracts/zeto_nf_anon_nullifier.sol +90 -152
  102. package/hardhat.config.ts +9 -3
  103. package/ignition/modules/lib/deps.ts +8 -0
  104. package/ignition/modules/zeto_anon_burnable.ts +8 -7
  105. package/ignition/modules/zeto_anon_enc_nullifier.ts +31 -0
  106. package/ignition/modules/zeto_anon_enc_nullifier_kyc.ts +2 -2
  107. package/ignition/modules/zeto_anon_nullifier.ts +28 -4
  108. package/ignition/modules/zeto_anon_nullifier_burnable.ts +50 -16
  109. package/ignition/modules/zeto_anon_nullifier_kyc.ts +27 -12
  110. package/ignition/modules/zeto_nf_anon_nullifier.ts +14 -10
  111. package/package.json +4 -2
  112. package/scripts/deploy_cloneable.ts +19 -10
  113. package/scripts/deploy_upgradeable.ts +9 -5
  114. package/scripts/tokens/Zeto_Anon.ts +3 -3
  115. package/scripts/tokens/Zeto_AnonBurnable.ts +1 -1
  116. package/scripts/tokens/Zeto_AnonEnc.ts +12 -3
  117. package/scripts/tokens/Zeto_AnonEncNullifier.ts +5 -3
  118. package/scripts/tokens/Zeto_AnonEncNullifierKyc.ts +1 -1
  119. package/scripts/tokens/Zeto_AnonEncNullifierNonRepudiation.ts +1 -1
  120. package/scripts/tokens/Zeto_AnonNullifier.ts +1 -1
  121. package/scripts/tokens/Zeto_AnonNullifierBurnable.ts +1 -1
  122. package/scripts/tokens/Zeto_AnonNullifierKyc.ts +1 -1
  123. package/scripts/tokens/Zeto_AnonNullifierQurrency.ts +1 -1
  124. package/scripts/tokens/Zeto_NfAnon.ts +2 -2
  125. package/scripts/tokens/Zeto_NfAnonNullifier.ts +1 -1
  126. package/test/factory.ts +46 -73
  127. package/test/lib/anon_nullifier_helpers.ts +89 -0
  128. package/test/lib/anon_zeto_helpers.ts +76 -0
  129. package/test/lib/deploy.ts +3 -2
  130. package/test/lib/utils.ts +74 -35
  131. package/test/test/escrow1.ts +185 -58
  132. package/test/test/escrow2.ts +200 -107
  133. package/test/test/qurrency.ts +13 -33
  134. package/test/usdc-shielding.ts +39 -26
  135. package/test/utils.ts +144 -21
  136. package/test/zeto_anon.ts +956 -465
  137. package/test/zeto_anon_enc.ts +881 -142
  138. package/test/zeto_anon_enc_nullifier.ts +847 -38
  139. package/test/zeto_anon_enc_nullifier_kyc.ts +120 -181
  140. package/test/zeto_anon_enc_nullifier_non_repudiation.ts +77 -46
  141. package/test/zeto_anon_nullifier.ts +1212 -953
  142. package/test/zeto_anon_nullifier_kyc.ts +674 -655
  143. package/test/zeto_anon_nullifier_qurrency.ts +452 -306
  144. package/test/zeto_nf_anon.ts +737 -137
  145. package/test/zeto_nf_anon_nullifier.ts +876 -246
  146. package/contracts/lib/zeto_base.sol +0 -293
  147. package/contracts/lib/zeto_fungible_withdraw.sol +0 -132
  148. package/contracts/lib/zeto_fungible_withdraw_nullifier.sol +0 -144
  149. package/contracts/lib/zeto_nullifier.sol +0 -340
  150. package/contracts/zkDvP.sol_ +0 -273
  151. package/test/zkDvP.ts_ +0 -455
  152. /package/contracts/lib/{common.sol → common/common.sol} +0 -0
@@ -15,10 +15,17 @@
15
15
  // limitations under the License.
16
16
 
17
17
  import { ethers, network } from "hardhat";
18
- import { ContractTransactionReceipt, Signer, BigNumberish } from "ethers";
18
+ import {
19
+ ContractTransactionReceipt,
20
+ Signer,
21
+ BigNumberish,
22
+ AbiCoder,
23
+ ZeroAddress,
24
+ } from "ethers";
19
25
  import { expect } from "chai";
20
26
  import { loadCircuit, Poseidon, encodeProof, tokenUriHash } from "zeto-js";
21
27
  import { groth16 } from "snarkjs";
28
+ import { formatPrivKeyForBabyJub, stringifyBigInts } from "maci-crypto";
22
29
  import { Merkletree, InMemoryDB, str2Bytes } from "@iden3/js-merkletree";
23
30
  import {
24
31
  UTXO,
@@ -28,8 +35,13 @@ import {
28
35
  newAssetNullifier,
29
36
  doMint,
30
37
  parseUTXOEvents,
38
+ logger,
31
39
  } from "./lib/utils";
32
- import { loadProvingKeys } from "./utils";
40
+ import {
41
+ loadProvingKeys,
42
+ calculateSpendHash,
43
+ calculateCancelHash,
44
+ } from "./utils";
33
45
  import { deployZeto } from "./lib/deploy";
34
46
 
35
47
  describe("Zeto based non-fungible token with anonymity using nullifiers without encryption", function () {
@@ -41,13 +53,18 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
41
53
  let utxo1: UTXO;
42
54
  let utxo2: UTXO;
43
55
  let circuit: any, provingKey: any;
56
+ // The locked-input transition reuses the simple `nf_anon` circuit
57
+ // (1-in/1-out, no nullifier, no merkle proof). Mirrors how
58
+ // {Zeto_AnonNullifier} reuses `anon` as its lock verifier — the
59
+ // contract storage layer already vouches that the input is in the
60
+ // locked-UTXO ledger, so the proof does not need to re-prove
61
+ // inclusion in any tree.
44
62
  let circuitLocked: any, provingKeyLocked: any;
45
63
  let smtAlice: Merkletree;
46
64
  let smtBob: Merkletree;
47
65
 
48
66
  before(async function () {
49
67
  if (network.name !== "hardhat") {
50
- // accommodate for longer block times on public networks
51
68
  this.timeout(120000);
52
69
  }
53
70
  let [d, a, b, c] = await ethers.getSigners();
@@ -62,10 +79,8 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
62
79
  ({ provingKeyFile: provingKey } = loadProvingKeys(
63
80
  "nf_anon_nullifier_transfer",
64
81
  ));
65
- circuitLocked = await loadCircuit("nf_anon_nullifier_transferLocked");
66
- ({ provingKeyFile: provingKeyLocked } = loadProvingKeys(
67
- "nf_anon_nullifier_transferLocked",
68
- ));
82
+ circuitLocked = await loadCircuit("nf_anon");
83
+ ({ provingKeyFile: provingKeyLocked } = loadProvingKeys("nf_anon"));
69
84
 
70
85
  const storage1 = new InMemoryDB(str2Bytes(""));
71
86
  smtAlice = new Merkletree(storage1, true, 64);
@@ -82,34 +97,24 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
82
97
  });
83
98
 
84
99
  it("mint to Alice and transfer UTXOs honestly to Bob should succeed", async function () {
85
- // The authority mints a new UTXO and assigns it to Alice
86
100
  const tokenId = 1001;
87
101
  const uri = "http://ipfs.io/file-hash-1";
88
102
  utxo1 = newAssetUTXO(tokenId, uri, Alice);
89
103
  const result1 = await doMint(zeto, deployer, [utxo1]);
90
104
 
91
- // Alice locally tracks the UTXOs inside the Sparse Merkle Tree
92
- // hardhat doesn't have a good way to subscribe to events so we have to parse the Tx result object
93
105
  const mintEvents = parseUTXOEvents(zeto, result1);
94
106
  const [_utxo1] = mintEvents[0].outputs;
95
107
  await smtAlice.add(_utxo1, _utxo1);
96
108
  let root = await smtAlice.root();
97
109
  let onchainRoot = await zeto.getRoot();
98
110
  expect(root.string()).to.equal(onchainRoot.toString());
99
- // Bob also locally tracks the UTXOs inside the Sparse Merkle Tree
100
111
  await smtBob.add(_utxo1, _utxo1);
101
112
 
102
- // Alice proposes the output UTXOs for the transfer to Bob
103
113
  const _utxo2 = newAssetUTXO(tokenId, uri, Bob);
104
-
105
- // Alice generates the nullifiers for the UTXOs to be spent
106
114
  const nullifier1 = newAssetNullifier(utxo1, Alice);
107
-
108
- // Alice generates inclusion proofs for the UTXOs to be spent
109
115
  const proof1 = await smtAlice.generateCircomVerifierProof(utxo1.hash, root);
110
116
  const merkleProof = proof1.siblings.map((s) => s.bigInt());
111
117
 
112
- // Alice transfers her UTXOs to Bob
113
118
  const result2 = await doTransfer(
114
119
  Alice,
115
120
  utxo1,
@@ -120,14 +125,11 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
120
125
  Bob,
121
126
  );
122
127
 
123
- // Alice locally tracks the UTXOs inside the Sparse Merkle Tree
124
128
  await smtAlice.add(_utxo2.hash, _utxo2.hash);
125
129
  root = await smtAlice.root();
126
130
  onchainRoot = await zeto.getRoot();
127
131
  expect(root.string()).to.equal(onchainRoot.toString());
128
132
 
129
- // Bob locally tracks the UTXOs inside the Sparse Merkle Tree
130
- // Bob parses the UTXOs from the onchain event
131
133
  const signerAddress = await Alice.signer.getAddress();
132
134
  const events = parseUTXOEvents(zeto, result2.txResult!);
133
135
  expect(events[0].submitter).to.equal(signerAddress);
@@ -135,7 +137,7 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
135
137
  expect(events[0].outputs).to.deep.equal([_utxo2.hash]);
136
138
  await smtBob.add(events[0].outputs[0], events[0].outputs[0]);
137
139
 
138
- // Bob uses the information received from Alice to reconstruct the UTXO sent to him
140
+ // Bob reconstructs the UTXO from the off-chain channel.
139
141
  const receivedTokenId = _utxo2.tokenId!;
140
142
  const receivedUri = _utxo2.uri!;
141
143
  const receivedSalt = _utxo2.salt;
@@ -149,23 +151,17 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
149
151
  ]);
150
152
  expect(incomingUTXOs[0]).to.equal(hash);
151
153
 
152
- // Bob uses the decrypted values to construct the UTXO received from the transaction
153
154
  utxo2 = newAssetUTXO(receivedTokenId, receivedUri, Bob, receivedSalt);
154
155
  }).timeout(600000);
155
156
 
156
157
  it("Bob transfers UTXOs, previously received from Alice, honestly to Charlie should succeed", async function () {
157
- // Bob generates the nullifiers for the UTXO to be spent
158
158
  const nullifier1 = newAssetNullifier(utxo2, Bob);
159
-
160
- // Bob generates inclusion proofs for the UTXOs to be spent, as private input to the proof generation
161
159
  const root = await smtBob.root();
162
160
  const proof1 = await smtBob.generateCircomVerifierProof(utxo2.hash, root);
163
161
  const merkleProof = proof1.siblings.map((s) => s.bigInt());
164
162
 
165
- // Bob proposes the output UTXOs
166
163
  const _utxo1 = newAssetUTXO(utxo2.tokenId!, utxo2.uri!, Charlie);
167
164
 
168
- // Bob should be able to spend the UTXO that was reconstructed from the previous transaction
169
165
  const result = await doTransfer(
170
166
  Bob,
171
167
  utxo2,
@@ -176,194 +172,13 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
176
172
  Charlie,
177
173
  );
178
174
 
179
- // Bob keeps the local SMT in sync
180
175
  await smtBob.add(_utxo1.hash, _utxo1.hash);
181
176
 
182
- // Alice gets the new UTXOs from the onchain event and keeps the local SMT in sync
183
177
  const events = parseUTXOEvents(zeto, result.txResult!);
184
178
  await smtAlice.add(events[0].outputs[0], events[0].outputs[0]);
185
179
  }).timeout(600000);
186
180
 
187
- describe("lock() tests", function () {
188
- let lockedUtxo: UTXO;
189
- let smtAliceLocked: Merkletree;
190
-
191
- before(async function () {
192
- const storage1 = new InMemoryDB(str2Bytes(""));
193
- smtAliceLocked = new Merkletree(storage1, true, 64);
194
- });
195
-
196
- it("lock a UTXO should succeed", async function () {
197
- const tokenId = 1002;
198
- const uri = "http://ipfs.io/file-hash-2";
199
- const _utxo1 = newAssetUTXO(tokenId, uri, Alice);
200
- await doMint(zeto, deployer, [_utxo1]);
201
-
202
- await smtAlice.add(_utxo1.hash, _utxo1.hash);
203
-
204
- const nullifier = newAssetNullifier(_utxo1, Alice);
205
- let root = await smtAlice.root();
206
- const proof1 = await smtAlice.generateCircomVerifierProof(
207
- _utxo1.hash,
208
- root,
209
- );
210
- const merkleProof = proof1.siblings.map((s) => s.bigInt());
211
- lockedUtxo = newAssetUTXO(_utxo1.tokenId!, _utxo1.uri!, Alice);
212
- const { outputCommitment, encodedProof } = await prepareProof(
213
- Alice,
214
- _utxo1,
215
- nullifier,
216
- lockedUtxo,
217
- root.bigInt(),
218
- merkleProof,
219
- Alice,
220
- );
221
- await expect(
222
- zeto
223
- .connect(Alice.signer)
224
- .lock(
225
- nullifier.hash,
226
- outputCommitment,
227
- root.bigInt(),
228
- encodedProof,
229
- Bob.ethAddress,
230
- "0x",
231
- ),
232
- ).to.be.fulfilled;
233
-
234
- await smtAliceLocked.add(
235
- lockedUtxo.hash,
236
- ethers.toBigInt(Bob.ethAddress),
237
- );
238
- });
239
-
240
- it("onchain SMT root for the locked UTXOs should be equal to the offchain SMT root", async function () {
241
- const root = await smtAliceLocked.root();
242
- const onchainRoot = await zeto.getRootForLocked();
243
- expect(root.string()).to.equal(onchainRoot.toString());
244
- });
245
-
246
- it("lock a UTXO that has already been locked should fail", async function () {
247
- const nullifier = newAssetNullifier(lockedUtxo, Alice);
248
- let root = await smtAliceLocked.root();
249
- const proof1 = await smtAliceLocked.generateCircomVerifierProof(
250
- lockedUtxo.hash,
251
- root,
252
- );
253
- const merkleProof = proof1.siblings.map((s) => s.bigInt());
254
- const _utxo1 = newAssetUTXO(lockedUtxo.tokenId!, lockedUtxo.uri!, Alice);
255
- const { outputCommitment, encodedProof } = await prepareProof(
256
- Alice,
257
- lockedUtxo,
258
- nullifier,
259
- _utxo1,
260
- root.bigInt(),
261
- merkleProof,
262
- Alice,
263
- Bob,
264
- );
265
- await expect(
266
- zeto
267
- .connect(Alice.signer)
268
- .lock(
269
- nullifier.hash,
270
- outputCommitment,
271
- root.bigInt(),
272
- encodedProof,
273
- Bob.ethAddress,
274
- "0x",
275
- ),
276
- ).to.be.rejectedWith("UTXORootNotFound");
277
- });
278
-
279
- it("the owner trying to spend a locked UTXO should fail", async function () {
280
- const nullifier = newAssetNullifier(lockedUtxo, Alice);
281
- let root = await smtAliceLocked.root();
282
- const proof1 = await smtAliceLocked.generateCircomVerifierProof(
283
- lockedUtxo.hash,
284
- root,
285
- );
286
- const merkleProof = proof1.siblings.map((s) => s.bigInt());
287
- const _utxo1 = newAssetUTXO(lockedUtxo.tokenId!, lockedUtxo.uri!, Bob);
288
- const { outputCommitment, encodedProof } = await prepareProof(
289
- Alice,
290
- lockedUtxo,
291
- nullifier,
292
- _utxo1,
293
- root.bigInt(),
294
- merkleProof,
295
- Bob,
296
- Bob,
297
- );
298
- await expect(
299
- zeto
300
- .connect(Alice.signer)
301
- .transfer(
302
- nullifier.hash,
303
- outputCommitment,
304
- root.bigInt(),
305
- encodedProof,
306
- "0x",
307
- ),
308
- ).to.be.rejectedWith("UTXORootNotFound");
309
- });
310
-
311
- it("the current delegate can move the lock to a new delegate", async function () {
312
- const tx = await zeto
313
- .connect(Bob.signer)
314
- .delegateLock([lockedUtxo.hash], Charlie.ethAddress, "0x");
315
- const result = await tx.wait();
316
- const events = parseUTXOEvents(zeto, result);
317
- // this should update the existing leaf node value from address of Alice to Charlie
318
- await smtAliceLocked.update(
319
- events[0].lockedOutputs[0],
320
- ethers.toBigInt(events[0].newDelegate),
321
- );
322
- });
323
-
324
- it("onchain SMT root for the locked UTXOs should be equal to the offchain SMT root", async function () {
325
- const root = await smtAliceLocked.root();
326
- const onchainRoot = await zeto.getRootForLocked();
327
- expect(root.string()).to.equal(onchainRoot.toString());
328
- });
329
-
330
- it("the new delegate can spend the locked UTXO using a valid proof from the current owner", async function () {
331
- const nullifier = newAssetNullifier(lockedUtxo, Alice);
332
- let root = await smtAliceLocked.root();
333
- const proof1 = await smtAliceLocked.generateCircomVerifierProof(
334
- lockedUtxo.hash,
335
- root,
336
- );
337
- const merkleProof = proof1.siblings.map((s) => s.bigInt());
338
- const _utxo1 = newAssetUTXO(lockedUtxo.tokenId!, lockedUtxo.uri!, Bob);
339
- const { outputCommitment, encodedProof } = await prepareProof(
340
- Alice,
341
- lockedUtxo,
342
- nullifier,
343
- _utxo1,
344
- root.bigInt(),
345
- merkleProof,
346
- Bob,
347
- Charlie,
348
- );
349
- await expect(
350
- zeto
351
- .connect(Charlie.signer)
352
- .transferLocked(
353
- nullifier.hash,
354
- outputCommitment,
355
- root.bigInt(),
356
- encodedProof,
357
- "0x",
358
- ),
359
- ).to.be.fulfilled;
360
- });
361
- });
362
-
363
181
  describe("failure cases", function () {
364
- // the following failure cases rely on the hardhat network
365
- // to return the details of the errors. This is not possible
366
- // on non-hardhat networks
367
182
  if (network.name !== "hardhat") {
368
183
  return;
369
184
  }
@@ -381,13 +196,9 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
381
196
  });
382
197
 
383
198
  it("transfer spent UTXOs should fail (double spend protection)", async function () {
384
- // Alice create outputs in an attempt to send to Charlie an already spent asset
385
199
  const _utxo1 = newAssetUTXO(utxo1.tokenId!, utxo1.uri!, Charlie);
386
-
387
- // generate the nullifiers for the UTXOs to be spent
388
200
  const nullifier1 = newAssetNullifier(utxo1, Alice);
389
201
 
390
- // generate inclusion proofs for the UTXOs to be spent
391
202
  let root = await smtAlice.root();
392
203
  const proof1 = await smtAlice.generateCircomVerifierProof(
393
204
  utxo1.hash,
@@ -410,18 +221,15 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
410
221
 
411
222
  it("transfer non-existing UTXOs should fail", async function () {
412
223
  const nonExisting1 = newAssetUTXO(
413
- 1002,
414
- "http://ipfs.io/file-hash-2",
224
+ 9001,
225
+ "http://ipfs.io/non-existing",
415
226
  Alice,
416
227
  );
417
228
 
418
229
  // add to our local SMT (but they don't exist on the chain)
419
230
  await smtAlice.add(nonExisting1.hash, nonExisting1.hash);
420
231
 
421
- // generate the nullifiers for the UTXOs to be spent
422
232
  const nullifier1 = newAssetNullifier(nonExisting1, Alice);
423
-
424
- // generate inclusion proofs for the UTXOs to be spent
425
233
  let root = await smtAlice.root();
426
234
  const proof1 = await smtAlice.generateCircomVerifierProof(
427
235
  nonExisting1.hash,
@@ -429,7 +237,6 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
429
237
  );
430
238
  const merkleProof = proof1.siblings.map((s) => s.bigInt());
431
239
 
432
- // propose the output UTXOs
433
240
  const _utxo1 = newAssetUTXO(
434
241
  nonExisting1.tokenId!,
435
242
  nonExisting1.uri!,
@@ -450,6 +257,765 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
450
257
  }).timeout(600000);
451
258
  });
452
259
 
260
+ describe("ILockableCapability tests", function () {
261
+ // ABI fragments mirror the layout used in zeto_anon_nullifier.ts;
262
+ // these payloads are uniform across all Zeto tokens that implement
263
+ // {ILockableCapability}.
264
+ const CREATE_ARGS_ABI =
265
+ "tuple(bytes32 txId, uint256[] inputs, uint256[] outputs, uint256[] lockedOutputs, bytes proof)";
266
+ const UPDATE_ARGS_ABI = "tuple(bytes32 txId)";
267
+ const DELEGATE_ARGS_ABI = "tuple(bytes32 txId)";
268
+ const SPEND_ARGS_ABI =
269
+ "tuple(bytes32 txId, uint256[] lockedOutputs, uint256[] outputs, bytes proof, bytes data)";
270
+
271
+ function encodeCreateArgs(args: {
272
+ txId: string;
273
+ inputs: BigNumberish[];
274
+ outputs: BigNumberish[];
275
+ lockedOutputs: BigNumberish[];
276
+ proof: string;
277
+ }) {
278
+ return new AbiCoder().encode([CREATE_ARGS_ABI], [args]);
279
+ }
280
+
281
+ function encodeUpdateArgs(txId: string) {
282
+ return new AbiCoder().encode([UPDATE_ARGS_ABI], [{ txId }]);
283
+ }
284
+
285
+ function encodeDelegateArgs(txId: string) {
286
+ return new AbiCoder().encode([DELEGATE_ARGS_ABI], [{ txId }]);
287
+ }
288
+
289
+ function encodeSpendArgs(args: {
290
+ txId: string;
291
+ lockedOutputs: BigNumberish[];
292
+ outputs: BigNumberish[];
293
+ proof: string;
294
+ data: string;
295
+ }) {
296
+ return new AbiCoder().encode([SPEND_ARGS_ABI], [args]);
297
+ }
298
+
299
+ function randomBytes32(): string {
300
+ return ethers.hexlify(ethers.randomBytes(32));
301
+ }
302
+
303
+ // mintAndLock mints a fresh UTXO under `owner`, runs createLock to
304
+ // convert it into a single locked UTXO of identical tokenId/uri,
305
+ // and returns everything a downstream test needs to drive
306
+ // update/delegate/spend/cancel without re-deriving any of it.
307
+ // Also mirrors the post-mint state into both SMTs so subsequent
308
+ // tests in the file that depend on tree consistency can keep
309
+ // ticking.
310
+ async function mintAndLock(
311
+ owner: User,
312
+ tokenId: number,
313
+ uri: string,
314
+ spendCommitment: string = ethers.ZeroHash,
315
+ cancelCommitment: string = ethers.ZeroHash,
316
+ ): Promise<{
317
+ lockId: string;
318
+ sourceUtxo: UTXO;
319
+ lockedUtxo: UTXO;
320
+ createArgs: string;
321
+ }> {
322
+ const sourceUtxo = newAssetUTXO(tokenId, uri, owner);
323
+ await doMint(zeto, deployer, [sourceUtxo]);
324
+ await smtAlice.add(sourceUtxo.hash, sourceUtxo.hash);
325
+ await smtBob.add(sourceUtxo.hash, sourceUtxo.hash);
326
+
327
+ const nullifier = newAssetNullifier(sourceUtxo, owner);
328
+ const root = await smtBob.root();
329
+ const proof = await smtBob.generateCircomVerifierProof(
330
+ sourceUtxo.hash,
331
+ root,
332
+ );
333
+ const merkleProof = proof.siblings.map((s) => s.bigInt());
334
+
335
+ const lockedUtxo = newAssetUTXO(tokenId, uri, owner);
336
+ const { encodedProof } = await prepareProof(
337
+ owner,
338
+ sourceUtxo,
339
+ nullifier,
340
+ lockedUtxo,
341
+ root.bigInt(),
342
+ merkleProof,
343
+ owner,
344
+ );
345
+
346
+ const createArgs = encodeCreateArgs({
347
+ txId: randomBytes32(),
348
+ inputs: [nullifier.hash],
349
+ outputs: [],
350
+ lockedOutputs: [lockedUtxo.hash],
351
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
352
+ });
353
+
354
+ const lockId = await zeto
355
+ .connect(owner.signer)
356
+ .computeLockId(createArgs);
357
+ await (
358
+ await zeto
359
+ .connect(owner.signer)
360
+ .createLock(createArgs, spendCommitment, cancelCommitment, "0x")
361
+ ).wait();
362
+
363
+ return { lockId, sourceUtxo, lockedUtxo, createArgs };
364
+ }
365
+
366
+ // dummySpendArgs returns a syntactically valid spend payload that
367
+ // does NOT need to verify a real ZK proof. Tests for authorization
368
+ // checks that short-circuit before the proof is touched can use
369
+ // this to avoid the cost of generating a real proof.
370
+ function dummySpendArgs(): string {
371
+ return encodeSpendArgs({
372
+ txId: randomBytes32(),
373
+ lockedOutputs: [],
374
+ outputs: [],
375
+ proof: "0x",
376
+ data: "0x",
377
+ });
378
+ }
379
+
380
+ describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
381
+ const tokenId = 2001;
382
+ const uri = "http://ipfs.io/lock-flow-1";
383
+ let bobSourceUtxo: UTXO;
384
+ let lockedUtxo: UTXO;
385
+ let lockId: string;
386
+ let outUtxo: UTXO;
387
+ let unlockHash: string;
388
+
389
+ it("createLock() with deterministic lockId computed from txId", async function () {
390
+ bobSourceUtxo = newAssetUTXO(tokenId, uri, Bob);
391
+ await doMint(zeto, deployer, [bobSourceUtxo]);
392
+ await smtAlice.add(bobSourceUtxo.hash, bobSourceUtxo.hash);
393
+ await smtBob.add(bobSourceUtxo.hash, bobSourceUtxo.hash);
394
+
395
+ const nullifier = newAssetNullifier(bobSourceUtxo, Bob);
396
+ const root = await smtBob.root();
397
+ const p = await smtBob.generateCircomVerifierProof(
398
+ bobSourceUtxo.hash,
399
+ root,
400
+ );
401
+ const merkleProof = p.siblings.map((s) => s.bigInt());
402
+
403
+ lockedUtxo = newAssetUTXO(tokenId, uri, Bob);
404
+ const { encodedProof } = await prepareProof(
405
+ Bob,
406
+ bobSourceUtxo,
407
+ nullifier,
408
+ lockedUtxo,
409
+ root.bigInt(),
410
+ merkleProof,
411
+ Bob,
412
+ );
413
+
414
+ const createArgs = encodeCreateArgs({
415
+ txId: randomBytes32(),
416
+ inputs: [nullifier.hash],
417
+ outputs: [],
418
+ lockedOutputs: [lockedUtxo.hash],
419
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
420
+ });
421
+
422
+ const predicted = await zeto
423
+ .connect(Bob.signer)
424
+ .computeLockId(createArgs);
425
+ lockId = predicted;
426
+
427
+ const tx = await zeto
428
+ .connect(Bob.signer)
429
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
430
+ const result: ContractTransactionReceipt | null = await tx.wait();
431
+ logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
432
+
433
+ const created = result!.logs
434
+ .map((l) => {
435
+ try {
436
+ return zeto.interface.parseLog(l as any);
437
+ } catch (_e) {
438
+ return null;
439
+ }
440
+ })
441
+ .find((p: any) => p && p.name === "LockCreated");
442
+ expect(created, "LockCreated event not found").to.not.be.null;
443
+ expect(created!.args.lockId).to.equal(predicted);
444
+ expect(created!.args.owner).to.equal(Bob.ethAddress);
445
+ expect(created!.args.spender).to.equal(Bob.ethAddress);
446
+ }).timeout(600000);
447
+
448
+ it("isLockActive() and getLock() reflect the newly created lock", async function () {
449
+ expect(await zeto.isLockActive(lockId)).to.equal(true);
450
+ const info = await zeto.getLock(lockId);
451
+ expect(info.owner).to.equal(Bob.ethAddress);
452
+ expect(info.spender).to.equal(Bob.ethAddress);
453
+ expect(info.spendCommitment).to.equal(ethers.ZeroHash);
454
+ expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
455
+ });
456
+
457
+ it("locked() returns (true, spender) for locked UTXOs and (false, 0) otherwise", async function () {
458
+ expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
459
+ true,
460
+ Bob.ethAddress,
461
+ ]);
462
+ // The source UTXO is consumed by createLock (its nullifier is
463
+ // recorded in `_nullifiers`) but the SMT entry remains -- the
464
+ // tree is append-only. From the ledger's point of view, the
465
+ // source hash is not locked.
466
+ expect((await zeto.locked(bobSourceUtxo.hash))[0]).to.be.false;
467
+ });
468
+
469
+ it("updateLock() commits the spend hash while owner == spender", async function () {
470
+ outUtxo = newAssetUTXO(tokenId, uri, Charlie);
471
+ unlockHash = calculateSpendHash([lockedUtxo], [], [outUtxo], "0x");
472
+
473
+ const tx = await zeto
474
+ .connect(Bob.signer)
475
+ .updateLock(
476
+ lockId,
477
+ encodeUpdateArgs(randomBytes32()),
478
+ unlockHash,
479
+ ethers.ZeroHash,
480
+ "0x",
481
+ );
482
+ const result = await tx.wait();
483
+ logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
484
+
485
+ const info = await zeto.getLock(lockId);
486
+ expect(info.spendCommitment).to.equal(unlockHash);
487
+ });
488
+
489
+ it("delegateLock() transfers spending authority to Alice", async function () {
490
+ const tx = await zeto
491
+ .connect(Bob.signer)
492
+ .delegateLock(
493
+ lockId,
494
+ encodeDelegateArgs(randomBytes32()),
495
+ Alice.ethAddress,
496
+ "0x",
497
+ );
498
+ const result = await tx.wait();
499
+ logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
500
+
501
+ const info = await zeto.getLock(lockId);
502
+ expect(info.spender).to.equal(Alice.ethAddress);
503
+ expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
504
+ true,
505
+ Alice.ethAddress,
506
+ ]);
507
+ });
508
+
509
+ it("the new spender can spendLock() with the matching payload", async function () {
510
+ // Locked-input transition: simple `nf_anon` proof against the
511
+ // locked UTXO hash.
512
+ const { encodedProof } = await prepareLockedProof(
513
+ Bob,
514
+ lockedUtxo,
515
+ outUtxo,
516
+ Charlie,
517
+ );
518
+
519
+ const spendArgs = encodeSpendArgs({
520
+ txId: randomBytes32(),
521
+ lockedOutputs: [],
522
+ outputs: [outUtxo.hash],
523
+ proof: encodeLockedProof(encodedProof),
524
+ data: "0x",
525
+ });
526
+
527
+ const tx = await zeto
528
+ .connect(Alice.signer)
529
+ .spendLock(lockId, spendArgs, "0x");
530
+ const result = await tx.wait();
531
+
532
+ const parsed = result!.logs
533
+ .map((l) => {
534
+ try {
535
+ return zeto.interface.parseLog(l as any);
536
+ } catch (_e) {
537
+ return null;
538
+ }
539
+ })
540
+ .filter((p: any) => p !== null) as ReadonlyArray<{
541
+ name: string;
542
+ args: any;
543
+ }>;
544
+ const lockSpent = parsed.find((p) => p.name === "LockSpent");
545
+ const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
546
+ expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
547
+ expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
548
+ .undefined;
549
+ expect(lockSpent!.args.lockId).to.equal(lockId);
550
+ expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
551
+
552
+ // Newly-unlocked output is added to the unlocked SMT by the
553
+ // contract; mirror it off-chain to keep the trees in sync.
554
+ await smtAlice.add(outUtxo.hash, outUtxo.hash);
555
+ await smtBob.add(outUtxo.hash, outUtxo.hash);
556
+
557
+ // Lock is consumed.
558
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
559
+ expect(await zeto.locked(lockedUtxo.hash)).to.deep.equal([
560
+ false,
561
+ ZeroAddress,
562
+ ]);
563
+ }).timeout(600000);
564
+
565
+ it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
566
+ const bobRoot = await smtBob.root();
567
+ const aliceRoot = await smtAlice.root();
568
+ const onchainRoot = await zeto.getRoot();
569
+ // smtAlice has been polluted by the failure-case test that
570
+ // appends a non-existing leaf; we don't compare it here. smtBob
571
+ // however should mirror the on-chain unlocked SMT.
572
+ expect(bobRoot.string()).to.equal(onchainRoot.toString());
573
+ expect(aliceRoot.string()).to.not.equal(onchainRoot.toString());
574
+ });
575
+ });
576
+
577
+ describe("createLock -> cancelLock flow", function () {
578
+ const tokenId = 2002;
579
+ const uri = "http://ipfs.io/lock-flow-2";
580
+ let lockedUtxo: UTXO;
581
+ let lockId: string;
582
+ let outUtxo: UTXO;
583
+ let cancelHash: string;
584
+
585
+ it("Bob createLock() with a non-zero cancelCommitment", async function () {
586
+ const sourceUtxo = newAssetUTXO(tokenId, uri, Bob);
587
+ await doMint(zeto, deployer, [sourceUtxo]);
588
+ await smtAlice.add(sourceUtxo.hash, sourceUtxo.hash);
589
+ await smtBob.add(sourceUtxo.hash, sourceUtxo.hash);
590
+
591
+ const nullifier = newAssetNullifier(sourceUtxo, Bob);
592
+ const root = await smtBob.root();
593
+ const p = await smtBob.generateCircomVerifierProof(
594
+ sourceUtxo.hash,
595
+ root,
596
+ );
597
+ const merkleProof = p.siblings.map((s) => s.bigInt());
598
+
599
+ lockedUtxo = newAssetUTXO(tokenId, uri, Bob);
600
+ const { encodedProof } = await prepareProof(
601
+ Bob,
602
+ sourceUtxo,
603
+ nullifier,
604
+ lockedUtxo,
605
+ root.bigInt(),
606
+ merkleProof,
607
+ Bob,
608
+ );
609
+
610
+ outUtxo = newAssetUTXO(tokenId, uri, Bob);
611
+ cancelHash = calculateCancelHash([lockedUtxo], [], [outUtxo], "0x");
612
+
613
+ const createArgs = encodeCreateArgs({
614
+ txId: randomBytes32(),
615
+ inputs: [nullifier.hash],
616
+ outputs: [],
617
+ lockedOutputs: [lockedUtxo.hash],
618
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
619
+ });
620
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
621
+ await (
622
+ await zeto
623
+ .connect(Bob.signer)
624
+ .createLock(createArgs, ethers.ZeroHash, cancelHash, "0x")
625
+ ).wait();
626
+ }).timeout(600000);
627
+
628
+ it("the owner can cancelLock() to reverse the lock without delegation", async function () {
629
+ const { encodedProof } = await prepareLockedProof(
630
+ Bob,
631
+ lockedUtxo,
632
+ outUtxo,
633
+ Bob,
634
+ );
635
+
636
+ const cancelArgs = encodeSpendArgs({
637
+ txId: randomBytes32(),
638
+ lockedOutputs: [],
639
+ outputs: [outUtxo.hash],
640
+ proof: encodeLockedProof(encodedProof),
641
+ data: "0x",
642
+ });
643
+
644
+ const tx = await zeto
645
+ .connect(Bob.signer)
646
+ .cancelLock(lockId, cancelArgs, "0x");
647
+ const result = await tx.wait();
648
+
649
+ const parsed = result!.logs
650
+ .map((l) => {
651
+ try {
652
+ return zeto.interface.parseLog(l as any);
653
+ } catch (_e) {
654
+ return null;
655
+ }
656
+ })
657
+ .filter((p: any) => p !== null) as ReadonlyArray<{
658
+ name: string;
659
+ args: any;
660
+ }>;
661
+ const cancelled = parsed.find((p) => p.name === "LockCancelled");
662
+ const zetoCancelled = parsed.find((p) => p.name === "ZetoLockCancelled");
663
+ expect(cancelled, "LockCancelled event not emitted").to.not.be
664
+ .undefined;
665
+ expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
666
+ .undefined;
667
+
668
+ await smtAlice.add(outUtxo.hash, outUtxo.hash);
669
+ await smtBob.add(outUtxo.hash, outUtxo.hash);
670
+
671
+ expect(await zeto.isLockActive(lockId)).to.equal(false);
672
+ }).timeout(600000);
673
+ });
674
+
675
+ describe("spendLock with a payload that does not match the spend commitment fails", function () {
676
+ const tokenId = 2003;
677
+ const uri = "http://ipfs.io/lock-flow-3";
678
+ let lockedUtxo: UTXO;
679
+ let lockId: string;
680
+ let expectedHash: string;
681
+
682
+ before(async function () {
683
+ const sourceUtxo = newAssetUTXO(tokenId, uri, Bob);
684
+ await doMint(zeto, deployer, [sourceUtxo]);
685
+ await smtAlice.add(sourceUtxo.hash, sourceUtxo.hash);
686
+ await smtBob.add(sourceUtxo.hash, sourceUtxo.hash);
687
+
688
+ const nullifier = newAssetNullifier(sourceUtxo, Bob);
689
+ const root = await smtBob.root();
690
+ const p = await smtBob.generateCircomVerifierProof(
691
+ sourceUtxo.hash,
692
+ root,
693
+ );
694
+ const merkleProof = p.siblings.map((s) => s.bigInt());
695
+
696
+ lockedUtxo = newAssetUTXO(tokenId, uri, Bob);
697
+ const { encodedProof } = await prepareProof(
698
+ Bob,
699
+ sourceUtxo,
700
+ nullifier,
701
+ lockedUtxo,
702
+ root.bigInt(),
703
+ merkleProof,
704
+ Bob,
705
+ );
706
+
707
+ const createArgs = encodeCreateArgs({
708
+ txId: randomBytes32(),
709
+ inputs: [nullifier.hash],
710
+ outputs: [],
711
+ lockedOutputs: [lockedUtxo.hash],
712
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
713
+ });
714
+ lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
715
+ await (
716
+ await zeto
717
+ .connect(Bob.signer)
718
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x")
719
+ ).wait();
720
+
721
+ const expectedOut = newAssetUTXO(tokenId, uri, Charlie);
722
+ expectedHash = calculateSpendHash(
723
+ [lockedUtxo],
724
+ [],
725
+ [expectedOut],
726
+ "0x",
727
+ );
728
+ await (
729
+ await zeto
730
+ .connect(Bob.signer)
731
+ .updateLock(
732
+ lockId,
733
+ encodeUpdateArgs(randomBytes32()),
734
+ expectedHash,
735
+ ethers.ZeroHash,
736
+ "0x",
737
+ )
738
+ ).wait();
739
+ });
740
+
741
+ it("spendLock() with a different payload reverts with InvalidUnlockHash", async function () {
742
+ if (network.name !== "hardhat") {
743
+ this.skip();
744
+ }
745
+ const wrongOut = newAssetUTXO(tokenId, uri, Alice);
746
+ const { encodedProof } = await prepareLockedProof(
747
+ Bob,
748
+ lockedUtxo,
749
+ wrongOut,
750
+ Alice,
751
+ );
752
+
753
+ const spendArgs = encodeSpendArgs({
754
+ txId: randomBytes32(),
755
+ lockedOutputs: [],
756
+ outputs: [wrongOut.hash],
757
+ proof: encodeLockedProof(encodedProof),
758
+ data: "0x",
759
+ });
760
+
761
+ const calculatedHash = calculateSpendHash(
762
+ [lockedUtxo],
763
+ [],
764
+ [wrongOut],
765
+ "0x",
766
+ );
767
+
768
+ await expect(
769
+ zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x"),
770
+ )
771
+ .to.be.revertedWithCustomError(zeto, "InvalidUnlockHash")
772
+ .withArgs(expectedHash, calculatedHash);
773
+ }).timeout(600000);
774
+ });
775
+
776
+ describe("locked inputs cannot be re-spent via plain transfer", function () {
777
+ // The unlocked-input transfer path runs through
778
+ // {NullifierStorage.validateInputs} with `inputsLocked=false`.
779
+ // For nullifier-based tokens, that path doesn't read the locked
780
+ // UTXO ledger directly (it only checks the `_nullifiers`
781
+ // mapping), so the locked-protection comes via a different
782
+ // mechanism: the off-chain attempt to construct a Merkle
783
+ // inclusion proof for the locked UTXO will fail because the
784
+ // locked output was never added to `_commitmentsTree`. Thus
785
+ // building the witness against `getRoot()` will produce a proof
786
+ // whose root the contract has never seen, surfacing as
787
+ // {UTXORootNotFound}. This is the nullifier-flavour analogue of
788
+ // the {AlreadyLocked} guard in the non-nullifier path.
789
+ if (network.name !== "hardhat") {
790
+ return;
791
+ }
792
+
793
+ it("transfer() of a locked UTXO reverts because the locked-UTXO root is not in the unlocked SMT", async function () {
794
+ const tokenId = 2010;
795
+ const uri = "http://ipfs.io/locked-no-transfer";
796
+ const { lockedUtxo } = await mintAndLock(Bob, tokenId, uri);
797
+
798
+ // Pretend off-chain we try to spend the locked UTXO via the
799
+ // public transfer() path. We add the locked UTXO to a local
800
+ // SMT (the contract never did) and then try to prove it.
801
+ const localStorage = new InMemoryDB(str2Bytes(""));
802
+ const localSmt = new Merkletree(localStorage, true, 64);
803
+ await localSmt.add(lockedUtxo.hash, lockedUtxo.hash);
804
+ const root = await localSmt.root();
805
+ const p = await localSmt.generateCircomVerifierProof(
806
+ lockedUtxo.hash,
807
+ root,
808
+ );
809
+ const merkleProof = p.siblings.map((s) => s.bigInt());
810
+
811
+ const nullifier = newAssetNullifier(lockedUtxo, Bob);
812
+ const otherUtxo = newAssetUTXO(tokenId, uri, Bob);
813
+ const result = await prepareProof(
814
+ Bob,
815
+ lockedUtxo,
816
+ nullifier,
817
+ otherUtxo,
818
+ root.bigInt(),
819
+ merkleProof,
820
+ Bob,
821
+ );
822
+
823
+ await expect(
824
+ zeto
825
+ .connect(Bob.signer)
826
+ .transfer(
827
+ nullifier.hash,
828
+ otherUtxo.hash,
829
+ encodeUnlockedProof(root.bigInt(), result.encodedProof),
830
+ "0x",
831
+ ),
832
+ ).rejectedWith("UTXORootNotFound");
833
+ }).timeout(600000);
834
+ });
835
+
836
+ describe("negative cases for the lock lifecycle", function () {
837
+ if (network.name !== "hardhat") {
838
+ return;
839
+ }
840
+
841
+ it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
842
+ const { lockId, createArgs } = await mintAndLock(
843
+ Bob,
844
+ 3001,
845
+ "http://ipfs.io/dup-lock",
846
+ );
847
+
848
+ // Same createArgs (same txId, same caller) => same lockId =>
849
+ // DuplicateLock fires before any input or proof validation.
850
+ await expect(
851
+ zeto
852
+ .connect(Bob.signer)
853
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
854
+ )
855
+ .to.be.revertedWithCustomError(zeto, "DuplicateLock")
856
+ .withArgs(lockId);
857
+ }).timeout(600000);
858
+
859
+ it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
860
+ const { lockId } = await mintAndLock(
861
+ Bob,
862
+ 3002,
863
+ "http://ipfs.io/non-owner-update",
864
+ );
865
+
866
+ await expect(
867
+ zeto
868
+ .connect(Alice.signer)
869
+ .updateLock(
870
+ lockId,
871
+ encodeUpdateArgs(randomBytes32()),
872
+ ethers.ZeroHash,
873
+ ethers.ZeroHash,
874
+ "0x",
875
+ ),
876
+ )
877
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
878
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
879
+ }).timeout(600000);
880
+
881
+ it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
882
+ const { lockId } = await mintAndLock(
883
+ Bob,
884
+ 3003,
885
+ "http://ipfs.io/immut-after-delegate",
886
+ );
887
+
888
+ await (
889
+ await zeto
890
+ .connect(Bob.signer)
891
+ .delegateLock(
892
+ lockId,
893
+ encodeDelegateArgs(randomBytes32()),
894
+ Alice.ethAddress,
895
+ "0x",
896
+ )
897
+ ).wait();
898
+
899
+ await expect(
900
+ zeto
901
+ .connect(Bob.signer)
902
+ .updateLock(
903
+ lockId,
904
+ encodeUpdateArgs(randomBytes32()),
905
+ ethers.ZeroHash,
906
+ ethers.ZeroHash,
907
+ "0x",
908
+ ),
909
+ )
910
+ .to.be.revertedWithCustomError(zeto, "LockImmutable")
911
+ .withArgs(lockId);
912
+ }).timeout(600000);
913
+
914
+ it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
915
+ const { lockId } = await mintAndLock(
916
+ Bob,
917
+ 3004,
918
+ "http://ipfs.io/non-spender-delegate",
919
+ );
920
+
921
+ await expect(
922
+ zeto
923
+ .connect(Alice.signer)
924
+ .delegateLock(
925
+ lockId,
926
+ encodeDelegateArgs(randomBytes32()),
927
+ Charlie.ethAddress,
928
+ "0x",
929
+ ),
930
+ )
931
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
932
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
933
+ }).timeout(600000);
934
+
935
+ it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
936
+ const { lockId } = await mintAndLock(
937
+ Bob,
938
+ 3005,
939
+ "http://ipfs.io/non-spender-spend",
940
+ );
941
+
942
+ await expect(
943
+ zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
944
+ )
945
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
946
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
947
+ }).timeout(600000);
948
+
949
+ it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
950
+ const { lockId } = await mintAndLock(
951
+ Bob,
952
+ 3006,
953
+ "http://ipfs.io/non-spender-cancel",
954
+ );
955
+
956
+ await expect(
957
+ zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
958
+ )
959
+ .to.be.revertedWithCustomError(zeto, "LockUnauthorized")
960
+ .withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
961
+ }).timeout(600000);
962
+
963
+ it("InvalidNonFungibleArity fires when createLock supplies more than one input", async function () {
964
+ const tokenId1 = 3010;
965
+ const tokenId2 = 3011;
966
+ const uri = "http://ipfs.io/arity-create";
967
+
968
+ const u1 = newAssetUTXO(tokenId1, uri, Bob);
969
+ const u2 = newAssetUTXO(tokenId2, uri + "2", Bob);
970
+ await doMint(zeto, deployer, [u1, u2]);
971
+ await smtBob.add(u1.hash, u1.hash);
972
+ await smtBob.add(u2.hash, u2.hash);
973
+ await smtAlice.add(u1.hash, u1.hash);
974
+ await smtAlice.add(u2.hash, u2.hash);
975
+
976
+ const nullifier1 = newAssetNullifier(u1, Bob);
977
+ const nullifier2 = newAssetNullifier(u2, Bob);
978
+ const root = await smtBob.root();
979
+ const p = await smtBob.generateCircomVerifierProof(u1.hash, root);
980
+ const merkleProof = p.siblings.map((s) => s.bigInt());
981
+
982
+ const lockedUtxo = newAssetUTXO(tokenId1, uri, Bob);
983
+ // Borrow a real proof for the (u1 -> lockedUtxo) transition;
984
+ // InvalidNonFungibleArity fires before the verifier is touched,
985
+ // but the payload still has to be well-formed bytes.
986
+ const { encodedProof } = await prepareProof(
987
+ Bob,
988
+ u1,
989
+ nullifier1,
990
+ lockedUtxo,
991
+ root.bigInt(),
992
+ merkleProof,
993
+ Bob,
994
+ );
995
+
996
+ const createArgs = encodeCreateArgs({
997
+ txId: randomBytes32(),
998
+ // Two inputs: invalid for the NF lock circuit -- a single
999
+ // token cannot be split.
1000
+ inputs: [nullifier1.hash, nullifier2.hash],
1001
+ outputs: [],
1002
+ lockedOutputs: [lockedUtxo.hash],
1003
+ proof: encodeUnlockedProof(root.bigInt(), encodedProof),
1004
+ });
1005
+
1006
+ await expect(
1007
+ zeto
1008
+ .connect(Bob.signer)
1009
+ .createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
1010
+ )
1011
+ .to.be.revertedWithCustomError(zeto, "InvalidNonFungibleArity")
1012
+ .withArgs(2, 0, 1);
1013
+ }).timeout(600000);
1014
+ });
1015
+ });
1016
+
1017
+ // --- helpers ---
1018
+
453
1019
  async function doTransfer(
454
1020
  signer: User,
455
1021
  input: UTXO,
@@ -459,9 +1025,6 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
459
1025
  merkleProof: BigInt[],
460
1026
  owner: User,
461
1027
  ) {
462
- let nullifier: BigNumberish;
463
- let outputCommitment: BigNumberish;
464
- let encodedProof: any;
465
1028
  const result = await prepareProof(
466
1029
  signer,
467
1030
  input,
@@ -471,20 +1034,18 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
471
1034
  merkleProof,
472
1035
  owner,
473
1036
  );
474
- nullifier = _nullifier.hash as BigNumberish;
475
- outputCommitment = result.outputCommitment;
476
- encodedProof = result.encodedProof;
477
-
478
1037
  const txResult = await sendTx(
479
1038
  signer,
480
- nullifier,
481
- outputCommitment,
1039
+ _nullifier.hash as BigNumberish,
1040
+ result.outputCommitment,
482
1041
  root,
483
- encodedProof,
1042
+ result.encodedProof,
484
1043
  );
485
1044
  return { txResult };
486
1045
  }
487
1046
 
1047
+ // prepareProof builds a (root, ZkProof) bundle for the unlocked-input
1048
+ // transition using the `nf_anon_nullifier_transfer` circuit.
488
1049
  async function prepareProof(
489
1050
  signer: User,
490
1051
  input: UTXO,
@@ -493,7 +1054,6 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
493
1054
  root: BigInt,
494
1055
  merkleProof: BigInt[],
495
1056
  owner: User,
496
- lockDelegate?: User,
497
1057
  ) {
498
1058
  const nullifier = _nullifier.hash as BigNumberish;
499
1059
  const inputCommitment: BigNumberish = input.hash as BigNumberish;
@@ -519,31 +1079,78 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
519
1079
  outputSalt,
520
1080
  outputOwnerPublicKey,
521
1081
  };
522
- if (lockDelegate) {
523
- inputObj["lockDelegate"] = ethers.toBigInt(lockDelegate.ethAddress);
524
- }
1082
+ const witness = await circuit.calculateWTNSBin(inputObj, true);
1083
+ const timeWitnessCalculation = Date.now() - startWitnessCalculation;
1084
+
1085
+ const startProofGeneration = Date.now();
1086
+ const { proof } = (await groth16.prove(provingKey, witness)) as {
1087
+ proof: BigNumberish[];
1088
+ publicSignals: BigNumberish[];
1089
+ };
1090
+ const timeProofGeneration = Date.now() - startProofGeneration;
525
1091
 
526
- let circuitToUse = lockDelegate ? circuitLocked : circuit;
527
- let provingKeyToUse = lockDelegate ? provingKeyLocked : provingKey;
528
- const witness = await circuitToUse.calculateWTNSBin(inputObj, true);
529
- const timeWithnessCalculation = Date.now() - startWitnessCalculation;
1092
+ logger.debug(
1093
+ `Witness calculation time: ${timeWitnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.`,
1094
+ );
1095
+
1096
+ return {
1097
+ inputCommitment,
1098
+ outputCommitment,
1099
+ encodedProof: encodeProof(proof),
1100
+ };
1101
+ }
1102
+
1103
+ // prepareLockedProof builds a ZkProof for the locked-input
1104
+ // transition using the simple `nf_anon` circuit. The locked input is
1105
+ // passed as a raw UTXO hash (not a nullifier); the contract's
1106
+ // {validateInputs(inputs, true)} confirms it sits in `_lockedUtxos`.
1107
+ async function prepareLockedProof(
1108
+ signer: User,
1109
+ input: UTXO,
1110
+ output: UTXO,
1111
+ to: User,
1112
+ ) {
1113
+ const tokenId = input.tokenId;
1114
+ const inputCommitment: BigNumberish = input.hash as BigNumberish;
1115
+ const inputSalt = input.salt;
1116
+ const outputCommitment: BigNumberish = output.hash as BigNumberish;
1117
+ const outputOwnerPublicKey: [BigNumberish, BigNumberish] =
1118
+ to.babyJubPublicKey as [BigNumberish, BigNumberish];
1119
+ const otherInputs = stringifyBigInts({
1120
+ inputOwnerPrivateKey: formatPrivKeyForBabyJub(signer.babyJubPrivateKey),
1121
+ });
1122
+
1123
+ const startWitnessCalculation = Date.now();
1124
+ const witness = await circuitLocked.calculateWTNSBin(
1125
+ {
1126
+ tokenIds: [tokenId],
1127
+ tokenUris: [tokenUriHash(input.uri)],
1128
+ inputCommitments: [inputCommitment],
1129
+ inputSalts: [inputSalt],
1130
+ outputCommitments: [outputCommitment],
1131
+ outputSalts: [output.salt],
1132
+ outputOwnerPublicKeys: [outputOwnerPublicKey],
1133
+ ...otherInputs,
1134
+ },
1135
+ true,
1136
+ );
1137
+ const timeWitnessCalculation = Date.now() - startWitnessCalculation;
530
1138
 
531
1139
  const startProofGeneration = Date.now();
532
- const { proof, publicSignals } = (await groth16.prove(
533
- provingKeyToUse,
534
- witness,
535
- )) as { proof: BigNumberish[]; publicSignals: BigNumberish[] };
1140
+ const { proof } = (await groth16.prove(provingKeyLocked, witness)) as {
1141
+ proof: BigNumberish[];
1142
+ publicSignals: BigNumberish[];
1143
+ };
536
1144
  const timeProofGeneration = Date.now() - startProofGeneration;
537
1145
 
538
- console.log(
539
- `Witness calculation time: ${timeWithnessCalculation}ms. Proof generation time: ${timeProofGeneration}ms.`,
1146
+ logger.debug(
1147
+ `Locked-witness calculation: ${timeWitnessCalculation}ms. Locked-proof generation: ${timeProofGeneration}ms.`,
540
1148
  );
541
1149
 
542
- const encodedProof = encodeProof(proof);
543
1150
  return {
544
1151
  inputCommitment,
545
1152
  outputCommitment,
546
- encodedProof,
1153
+ encodedProof: encodeProof(proof),
547
1154
  };
548
1155
  }
549
1156
 
@@ -557,9 +1164,14 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
557
1164
  const startTx = Date.now();
558
1165
  const tx = await zeto
559
1166
  .connect(signer.signer)
560
- .transfer(nullifier, outputCommitment, root, encodedProof, "0x");
1167
+ .transfer(
1168
+ nullifier,
1169
+ outputCommitment,
1170
+ encodeUnlockedProof(root, encodedProof),
1171
+ "0x",
1172
+ );
561
1173
  const results: ContractTransactionReceipt | null = await tx.wait();
562
- console.log(
1174
+ logger.debug(
563
1175
  `Time to execute transaction: ${Date.now() - startTx}ms. Gas used: ${
564
1176
  results?.gasUsed
565
1177
  }`,
@@ -567,3 +1179,21 @@ describe("Zeto based non-fungible token with anonymity using nullifiers without
567
1179
  return results;
568
1180
  }
569
1181
  });
1182
+
1183
+ // Wire-format helpers. The unlocked branch carries `(root, ZkProof)`
1184
+ // in the proof bytes; the locked branch carries just the ZkProof.
1185
+ // These match the two `abi.decode` arms in
1186
+ // {Zeto_NfAnonNullifier.constructPublicInputs}.
1187
+ function encodeUnlockedProof(root: any, proof: any) {
1188
+ return new AbiCoder().encode(
1189
+ ["uint256 root", "tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)"],
1190
+ [root, proof],
1191
+ );
1192
+ }
1193
+
1194
+ function encodeLockedProof(proof: any) {
1195
+ return new AbiCoder().encode(
1196
+ ["tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)"],
1197
+ [proof],
1198
+ );
1199
+ }