@lfdecentralizedtrust/zeto-contracts 0.2.2 → 0.5.1

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