@lfdecentralizedtrust/zeto-contracts 0.2.1 → 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.
- package/README.md +70 -162
- package/contracts/factory.sol +30 -34
- package/contracts/factory_upgradeable.sol +160 -0
- package/contracts/lib/common/util.sol +40 -0
- package/contracts/lib/interfaces/ILockableCapability.sol +318 -0
- package/contracts/lib/interfaces/{izeto.sol → IZeto.sol} +13 -1
- package/contracts/lib/interfaces/{izeto_initializable.sol → IZetoInitializable.sol} +14 -12
- package/contracts/lib/interfaces/{izeto_kyc.sol → IZetoKyc.sol} +3 -3
- package/contracts/lib/interfaces/IZetoLockableCapability.sol +237 -0
- package/contracts/lib/interfaces/IZetoStorage.sol +106 -0
- package/contracts/lib/interfaces/{izeto_lockable.sol → IZetoVerifier.sol} +8 -21
- package/contracts/lib/registry.sol +77 -27
- package/contracts/lib/storage/base.sol +210 -0
- package/contracts/lib/storage/nullifier.sol +166 -0
- package/contracts/lib/zeto_common.sol +277 -28
- package/contracts/lib/zeto_fungible.sol +451 -33
- package/contracts/lib/zeto_fungible_base.sol +69 -0
- package/contracts/lib/zeto_fungible_burn.sol +83 -53
- package/contracts/lib/zeto_fungible_burn_nullifier.sol +115 -72
- package/contracts/lib/zeto_fungible_nullifier.sol +167 -0
- package/contracts/lib/zeto_lockable.sol +613 -0
- package/contracts/lib/zeto_non_fungible.sol +236 -0
- package/contracts/lib/zeto_non_fungible_base.sol +61 -0
- package/contracts/lib/zeto_non_fungible_nullifier.sol +61 -0
- package/contracts/test/escrow1.sol +90 -34
- package/contracts/test/escrow2.sol +64 -29
- package/contracts/test/qurrency.sol +10 -2
- package/contracts/test/smt.sol +8 -3
- package/contracts/test/tendecimals.sol +14 -12
- package/contracts/verifiers/impl/anon.sol +189 -0
- package/contracts/verifiers/impl/anon_batch.sol +301 -0
- package/contracts/verifiers/impl/anon_enc.sol +266 -0
- package/contracts/verifiers/impl/anon_enc_batch.sol +602 -0
- package/contracts/verifiers/impl/anon_enc_nullifier.sol +287 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_batch.sol +679 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_kyc.sol +294 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_kyc_batch.sol +686 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation.sol +413 -0
- package/contracts/verifiers/impl/anon_enc_nullifier_non_repudiation_batch.sol +1141 -0
- package/contracts/verifiers/impl/anon_nullifier_kyc_transfer.sol +217 -0
- package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked.sol +196 -0
- package/contracts/verifiers/impl/anon_nullifier_kyc_transferLocked_batch.sol +308 -0
- package/contracts/verifiers/impl/anon_nullifier_kyc_transfer_batch.sol +385 -0
- package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer.sol +497 -0
- package/contracts/verifiers/impl/anon_nullifier_qurrency_transfer_batch.sol +1001 -0
- package/contracts/verifiers/{verifier_anon_nullifier_transferLocked.sol → impl/anon_nullifier_transfer.sol} +12 -19
- package/contracts/verifiers/{verifier_anon_nullifier_transferLocked_batch.sol → impl/anon_nullifier_transfer_batch.sol} +44 -51
- package/contracts/verifiers/impl/burn.sol +182 -0
- package/contracts/verifiers/impl/burn_batch.sol +238 -0
- package/contracts/verifiers/impl/burn_nullifier.sol +203 -0
- package/contracts/verifiers/impl/burn_nullifier_batch.sol +315 -0
- package/contracts/verifiers/impl/deposit.sol +182 -0
- package/contracts/verifiers/impl/deposit_kyc.sol +189 -0
- package/contracts/verifiers/impl/nf_anon.sol +175 -0
- package/contracts/verifiers/{verifier_nf_anon_nullifier_transferLocked.sol → impl/nf_anon_nullifier_transfer.sol} +6 -13
- package/contracts/verifiers/impl/withdraw.sol +189 -0
- package/contracts/verifiers/impl/withdraw_batch.sol +245 -0
- package/contracts/verifiers/impl/withdraw_nullifier.sol +210 -0
- package/contracts/verifiers/impl/withdraw_nullifier_batch.sol +322 -0
- package/contracts/verifiers/verifier_anon.sol +31 -186
- package/contracts/verifiers/verifier_anon_batch.sol +31 -298
- package/contracts/verifiers/verifier_anon_enc.sol +31 -263
- package/contracts/verifiers/verifier_anon_enc_batch.sol +31 -599
- package/contracts/verifiers/verifier_anon_enc_nullifier.sol +31 -284
- package/contracts/verifiers/verifier_anon_enc_nullifier_batch.sol +33 -676
- package/contracts/verifiers/verifier_anon_enc_nullifier_kyc.sol +31 -291
- package/contracts/verifiers/verifier_anon_enc_nullifier_kyc_batch.sol +33 -683
- package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation.sol +33 -410
- package/contracts/verifiers/verifier_anon_enc_nullifier_non_repudiation_batch.sol +33 -1138
- package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer.sol +33 -214
- package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked.sol +33 -221
- package/contracts/verifiers/verifier_anon_nullifier_kyc_transferLocked_batch.sol +33 -389
- package/contracts/verifiers/verifier_anon_nullifier_kyc_transfer_batch.sol +33 -382
- package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer.sol +33 -221
- package/contracts/verifiers/verifier_anon_nullifier_qurrency_transfer_batch.sol +33 -389
- package/contracts/verifiers/verifier_anon_nullifier_transfer.sol +33 -207
- package/contracts/verifiers/verifier_anon_nullifier_transfer_batch.sol +33 -375
- package/contracts/verifiers/verifier_burn.sol +31 -179
- package/contracts/verifiers/verifier_burn_batch.sol +31 -235
- package/contracts/verifiers/verifier_burn_nullifier.sol +31 -200
- package/contracts/verifiers/verifier_burn_nullifier_batch.sol +31 -312
- package/contracts/verifiers/verifier_deposit.sol +31 -179
- package/contracts/verifiers/verifier_deposit_kyc.sol +34 -0
- package/contracts/verifiers/verifier_nf_anon.sol +31 -172
- package/contracts/verifiers/verifier_nf_anon_nullifier_transfer.sol +33 -179
- package/contracts/verifiers/verifier_withdraw.sol +31 -186
- package/contracts/verifiers/verifier_withdraw_batch.sol +31 -242
- package/contracts/verifiers/verifier_withdraw_nullifier.sol +31 -207
- package/contracts/verifiers/verifier_withdraw_nullifier_batch.sol +33 -319
- package/contracts/zeto_anon.sol +77 -231
- package/contracts/zeto_anon_burnable.sol +56 -12
- package/contracts/zeto_anon_enc.sol +93 -190
- package/contracts/zeto_anon_enc_nullifier.sol +249 -195
- package/contracts/zeto_anon_enc_nullifier_kyc.sol +51 -232
- package/contracts/zeto_anon_enc_nullifier_non_repudiation.sol +231 -238
- package/contracts/zeto_anon_nullifier.sol +164 -298
- package/contracts/zeto_anon_nullifier_burnable.sol +68 -18
- package/contracts/zeto_anon_nullifier_kyc.sol +40 -345
- package/contracts/zeto_anon_nullifier_qurrency.sol +218 -360
- package/contracts/zeto_nf_anon.sol +55 -130
- package/contracts/zeto_nf_anon_nullifier.sol +90 -152
- package/hardhat.config.ts +9 -3
- package/ignition/modules/lib/deps.ts +9 -6
- package/ignition/modules/zeto_anon_burnable.ts +8 -7
- package/ignition/modules/zeto_anon_enc_nullifier.ts +33 -1
- package/ignition/modules/zeto_anon_enc_nullifier_kyc.ts +2 -2
- package/ignition/modules/zeto_anon_enc_nullifier_non_repudiation.ts +2 -1
- package/ignition/modules/zeto_anon_nullifier.ts +30 -5
- package/ignition/modules/zeto_anon_nullifier_burnable.ts +52 -17
- package/ignition/modules/zeto_anon_nullifier_kyc.ts +27 -12
- package/ignition/modules/zeto_anon_nullifier_qurrency.ts +2 -1
- package/ignition/modules/zeto_nf_anon_nullifier.ts +16 -12
- package/package.json +4 -2
- package/scripts/deploy_cloneable.ts +19 -10
- package/scripts/deploy_upgradeable.ts +9 -5
- package/scripts/tokens/Zeto_Anon.ts +3 -3
- package/scripts/tokens/Zeto_AnonBurnable.ts +1 -1
- package/scripts/tokens/Zeto_AnonEnc.ts +12 -3
- package/scripts/tokens/Zeto_AnonEncNullifier.ts +7 -3
- package/scripts/tokens/Zeto_AnonEncNullifierKyc.ts +1 -1
- package/scripts/tokens/Zeto_AnonEncNullifierNonRepudiation.ts +3 -1
- package/scripts/tokens/Zeto_AnonNullifier.ts +3 -1
- package/scripts/tokens/Zeto_AnonNullifierBurnable.ts +3 -1
- package/scripts/tokens/Zeto_AnonNullifierKyc.ts +1 -1
- package/scripts/tokens/Zeto_AnonNullifierQurrency.ts +3 -1
- package/scripts/tokens/Zeto_NfAnon.ts +2 -2
- package/scripts/tokens/Zeto_NfAnonNullifier.ts +3 -2
- package/test/factory.ts +46 -73
- package/test/factory_upgradeable.ts +311 -0
- package/test/lib/anon_nullifier_helpers.ts +89 -0
- package/test/lib/anon_zeto_helpers.ts +76 -0
- package/test/lib/deploy.ts +3 -2
- package/test/lib/utils.ts +74 -35
- package/test/test/escrow1.ts +185 -58
- package/test/test/escrow2.ts +200 -107
- package/test/test/qurrency.ts +13 -33
- package/test/usdc-shielding.ts +39 -26
- package/test/utils.ts +144 -21
- package/test/zeto_anon.ts +956 -465
- package/test/zeto_anon_enc.ts +881 -142
- package/test/zeto_anon_enc_nullifier.ts +847 -38
- package/test/zeto_anon_enc_nullifier_kyc.ts +120 -181
- package/test/zeto_anon_enc_nullifier_non_repudiation.ts +77 -46
- package/test/zeto_anon_nullifier.ts +1212 -953
- package/test/zeto_anon_nullifier_kyc.ts +674 -655
- package/test/zeto_anon_nullifier_qurrency.ts +452 -306
- package/test/zeto_nf_anon.ts +737 -137
- package/test/zeto_nf_anon_nullifier.ts +876 -246
- package/contracts/lib/zeto_base.sol +0 -293
- package/contracts/lib/zeto_fungible_withdraw.sol +0 -132
- package/contracts/lib/zeto_fungible_withdraw_nullifier.sol +0 -144
- package/contracts/lib/zeto_nullifier.sol +0 -334
- package/contracts/zkDvP.sol_ +0 -273
- package/test/zkDvP.ts_ +0 -455
- /package/contracts/lib/{common.sol → common/common.sol} +0 -0
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
// limitations under the License.
|
|
16
16
|
|
|
17
17
|
import { ethers, network } from "hardhat";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
ContractTransactionReceipt,
|
|
20
|
+
Signer,
|
|
21
|
+
BigNumberish,
|
|
22
|
+
AbiCoder,
|
|
23
|
+
ZeroAddress,
|
|
24
|
+
} from "ethers";
|
|
19
25
|
import { expect } from "chai";
|
|
20
26
|
import {
|
|
21
27
|
loadCircuit,
|
|
@@ -41,13 +47,22 @@ import {
|
|
|
41
47
|
doMint,
|
|
42
48
|
ZERO_UTXO,
|
|
43
49
|
parseUTXOEvents,
|
|
50
|
+
logger,
|
|
44
51
|
} from "./lib/utils";
|
|
45
52
|
import {
|
|
46
53
|
loadProvingKeys,
|
|
47
54
|
prepareDepositProof,
|
|
48
|
-
prepareNullifiersLockProof,
|
|
49
55
|
prepareNullifierWithdrawProof,
|
|
56
|
+
encodeToBytesForDeposit,
|
|
57
|
+
encodeToBytesForWithdraw,
|
|
58
|
+
calculateSpendHash,
|
|
59
|
+
calculateCancelHash,
|
|
50
60
|
} from "./utils";
|
|
61
|
+
process.env.SKIP_ANON_ENC_TESTS = "true";
|
|
62
|
+
import {
|
|
63
|
+
prepareProof as prepareProofForLockedEnc,
|
|
64
|
+
encodeToBytes as encodeToBytesForLockedEnc,
|
|
65
|
+
} from "./zeto_anon_enc";
|
|
51
66
|
import { deployZeto } from "./lib/deploy";
|
|
52
67
|
const poseidonHash = Poseidon.poseidon4;
|
|
53
68
|
|
|
@@ -65,6 +80,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
65
80
|
let utxo4: UTXO;
|
|
66
81
|
let utxo7: UTXO;
|
|
67
82
|
let circuit: any, provingKey: any;
|
|
83
|
+
let circuitForLocked: any, provingKeyForLocked: any;
|
|
68
84
|
let batchCircuit: any, batchProvingKey: any;
|
|
69
85
|
let smtAlice: Merkletree;
|
|
70
86
|
let smtBob: Merkletree;
|
|
@@ -94,6 +110,18 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
94
110
|
({ provingKeyFile: batchProvingKey } = loadProvingKeys(
|
|
95
111
|
"anon_enc_nullifier_batch",
|
|
96
112
|
));
|
|
113
|
+
// For consuming locked UTXOs we use the non-nullifier `anon_enc`
|
|
114
|
+
// circuit. Rationale (mirrors {Zeto_AnonNullifier}): under the
|
|
115
|
+
// ILockableCapability storage, locked UTXOs live in a flat per-lock
|
|
116
|
+
// mapping with no SMT or nullifier history, so the locked-input proof
|
|
117
|
+
// has nothing to bind against on the nullifier side. The encryption
|
|
118
|
+
// witness is still required (receiver data availability), so we use
|
|
119
|
+
// `anon_enc` rather than the plain `anon` circuit. The contract's
|
|
120
|
+
// {constructPublicInputs(..., inputsLocked = true)} emits the
|
|
121
|
+
// `[ecdhPublicKey, encryptedValues, inputs, outputs, encryptionNonce]`
|
|
122
|
+
// layout that `Groth16Verifier_AnonEnc` expects.
|
|
123
|
+
circuitForLocked = await loadCircuit("anon_enc");
|
|
124
|
+
({ provingKeyFile: provingKeyForLocked } = loadProvingKeys("anon_enc"));
|
|
97
125
|
});
|
|
98
126
|
|
|
99
127
|
it("onchain SMT root should be equal to the offchain SMT root", async function () {
|
|
@@ -164,12 +192,13 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
164
192
|
|
|
165
193
|
const signerAddress = await Alice.signer.getAddress();
|
|
166
194
|
const events = parseUTXOEvents(zeto, result.txResult!);
|
|
167
|
-
|
|
168
|
-
expect(
|
|
195
|
+
const event = events[0];
|
|
196
|
+
expect(event.submitter).to.equal(signerAddress);
|
|
197
|
+
expect(event.inputs).to.deep.equal(nullifiers.map((n) => n.hash));
|
|
169
198
|
|
|
170
|
-
const incomingUTXOs: any =
|
|
199
|
+
const incomingUTXOs: any = event.outputs;
|
|
171
200
|
|
|
172
|
-
const ecdhPublicKey =
|
|
201
|
+
const ecdhPublicKey = event.ecdhPublicKey;
|
|
173
202
|
|
|
174
203
|
// check the non-empty output hashes are correct
|
|
175
204
|
for (let i = 0; i < outputUtxos.length; i++) {
|
|
@@ -179,9 +208,9 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
179
208
|
ecdhPublicKey,
|
|
180
209
|
);
|
|
181
210
|
const plainText = poseidonDecrypt(
|
|
182
|
-
|
|
211
|
+
event.encryptedValues.slice(4 * i, 4 * i + 4),
|
|
183
212
|
sharedKey,
|
|
184
|
-
|
|
213
|
+
event.encryptionNonce,
|
|
185
214
|
2,
|
|
186
215
|
);
|
|
187
216
|
expect(plainText).to.deep.equal(
|
|
@@ -198,11 +227,6 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
198
227
|
await smtBob.add(incomingUTXOs[i], incomingUTXOs[i]);
|
|
199
228
|
}
|
|
200
229
|
|
|
201
|
-
// check empty values, salt and hashes are empty
|
|
202
|
-
for (let i = outputUtxos.length; i < 10; i++) {
|
|
203
|
-
expect(incomingUTXOs[i]).to.equal(0);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
230
|
// mint sufficient balance in Zeto contract address for Alice to withdraw
|
|
207
231
|
const mintTx = await erc20.connect(deployer).mint(zeto, 3);
|
|
208
232
|
await mintTx.wait();
|
|
@@ -255,8 +279,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
255
279
|
3,
|
|
256
280
|
_withdrawNullifiers,
|
|
257
281
|
withdrawCommitments[0],
|
|
258
|
-
root.bigInt(),
|
|
259
|
-
withdrawEncodedProof,
|
|
282
|
+
encodeToBytesForWithdraw(root.bigInt(), withdrawEncodedProof),
|
|
260
283
|
"0x",
|
|
261
284
|
);
|
|
262
285
|
await tx.wait();
|
|
@@ -264,7 +287,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
264
287
|
// Alice checks her ERC20 balance
|
|
265
288
|
const endingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
266
289
|
expect(endingBalance - startingBalance).to.be.equal(3);
|
|
267
|
-
}).timeout(
|
|
290
|
+
}).timeout(180000);
|
|
268
291
|
|
|
269
292
|
it("mint ERC20 tokens to Alice to deposit to Zeto should succeed", async function () {
|
|
270
293
|
const startingBalance = await erc20.balanceOf(Alice.ethAddress);
|
|
@@ -284,7 +307,12 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
284
307
|
);
|
|
285
308
|
const tx2 = await zeto
|
|
286
309
|
.connect(Alice.signer)
|
|
287
|
-
.deposit(
|
|
310
|
+
.deposit(
|
|
311
|
+
100,
|
|
312
|
+
outputCommitments,
|
|
313
|
+
encodeToBytesForDeposit(encodedProof),
|
|
314
|
+
"0x",
|
|
315
|
+
);
|
|
288
316
|
await tx2.wait();
|
|
289
317
|
|
|
290
318
|
await smtAlice.add(utxo100.hash, utxo100.hash);
|
|
@@ -359,20 +387,21 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
359
387
|
// Bob parses the UTXOs from the onchain event
|
|
360
388
|
const signerAddress = await Alice.signer.getAddress();
|
|
361
389
|
const events = parseUTXOEvents(zeto, result2.txResult!);
|
|
362
|
-
|
|
363
|
-
expect(
|
|
364
|
-
expect(
|
|
365
|
-
|
|
366
|
-
await smtBob.add(
|
|
367
|
-
|
|
368
|
-
|
|
390
|
+
const event = events[0];
|
|
391
|
+
expect(event.submitter).to.equal(signerAddress);
|
|
392
|
+
expect(event.inputs).to.deep.equal([nullifier1.hash, nullifier2.hash]);
|
|
393
|
+
expect(event.outputs).to.deep.equal([_utxo3.hash, utxo4.hash]);
|
|
394
|
+
await smtBob.add(event.outputs[0], event.outputs[0]);
|
|
395
|
+
await smtBob.add(event.outputs[1], event.outputs[1]);
|
|
396
|
+
|
|
397
|
+
const ecdhPublicKey = event.ecdhPublicKey;
|
|
369
398
|
// Bob reconstructs the shared key using his private key and ephemeral public key
|
|
370
399
|
|
|
371
400
|
const sharedKey = genEcdhSharedKey(Bob.babyJubPrivateKey, ecdhPublicKey);
|
|
372
401
|
const plainText = poseidonDecrypt(
|
|
373
|
-
|
|
402
|
+
event.encryptedValues.slice(0, 4),
|
|
374
403
|
sharedKey,
|
|
375
|
-
|
|
404
|
+
event.encryptionNonce,
|
|
376
405
|
2,
|
|
377
406
|
);
|
|
378
407
|
expect(plainText).to.deep.equal(result2.expectedPlainText.slice(0, 2));
|
|
@@ -415,8 +444,9 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
415
444
|
|
|
416
445
|
// Alice gets the new UTXOs from the onchain event and keeps the local SMT in sync
|
|
417
446
|
const events = parseUTXOEvents(zeto, result.txResult!);
|
|
418
|
-
|
|
419
|
-
await smtAlice.add(
|
|
447
|
+
const event = events[0];
|
|
448
|
+
await smtAlice.add(event.outputs[0], event.outputs[0]);
|
|
449
|
+
await smtAlice.add(event.outputs[1], event.outputs[1]);
|
|
420
450
|
}).timeout(600000);
|
|
421
451
|
|
|
422
452
|
it("Alice withdraws her UTXOs to ERC20 tokens should succeed", async function () {
|
|
@@ -457,8 +487,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
457
487
|
80,
|
|
458
488
|
nullifiers,
|
|
459
489
|
outputCommitments[0],
|
|
460
|
-
root.bigInt(),
|
|
461
|
-
encodedProof,
|
|
490
|
+
encodeToBytesForWithdraw(root.bigInt(), encodedProof),
|
|
462
491
|
"0x",
|
|
463
492
|
);
|
|
464
493
|
await tx.wait();
|
|
@@ -473,7 +502,767 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
473
502
|
expect(endingBalance - startingBalance).to.be.equal(80);
|
|
474
503
|
});
|
|
475
504
|
|
|
476
|
-
|
|
505
|
+
describe("ILockableCapability tests", function () {
|
|
506
|
+
// ABI fragments for the ZetoLockableCapability *Args payloads.
|
|
507
|
+
// Identical to the layout used by every other Zeto fungible token —
|
|
508
|
+
// only the contents of the opaque `proof` blob differ between tokens.
|
|
509
|
+
const CREATE_ARGS_ABI =
|
|
510
|
+
"tuple(bytes32 txId, uint256[] inputs, uint256[] outputs, uint256[] lockedOutputs, bytes proof)";
|
|
511
|
+
const UPDATE_ARGS_ABI = "tuple(bytes32 txId)";
|
|
512
|
+
const DELEGATE_ARGS_ABI = "tuple(bytes32 txId)";
|
|
513
|
+
const SPEND_ARGS_ABI =
|
|
514
|
+
"tuple(bytes32 txId, uint256[] lockedOutputs, uint256[] outputs, bytes proof, bytes data)";
|
|
515
|
+
|
|
516
|
+
function encodeCreateArgs(args: {
|
|
517
|
+
txId: string;
|
|
518
|
+
inputs: BigNumberish[];
|
|
519
|
+
outputs: BigNumberish[];
|
|
520
|
+
lockedOutputs: BigNumberish[];
|
|
521
|
+
proof: string;
|
|
522
|
+
}) {
|
|
523
|
+
return new AbiCoder().encode([CREATE_ARGS_ABI], [args]);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function encodeUpdateArgs(txId: string) {
|
|
527
|
+
return new AbiCoder().encode([UPDATE_ARGS_ABI], [{ txId }]);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function encodeDelegateArgs(txId: string) {
|
|
531
|
+
return new AbiCoder().encode([DELEGATE_ARGS_ABI], [{ txId }]);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function encodeSpendArgs(args: {
|
|
535
|
+
txId: string;
|
|
536
|
+
lockedOutputs: BigNumberish[];
|
|
537
|
+
outputs: BigNumberish[];
|
|
538
|
+
proof: string;
|
|
539
|
+
data: string;
|
|
540
|
+
}) {
|
|
541
|
+
return new AbiCoder().encode([SPEND_ARGS_ABI], [args]);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function randomBytes32(): string {
|
|
545
|
+
return ethers.hexlify(ethers.randomBytes(32));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// prepareCreateLockProofBytes generates an `anon_enc_nullifier` proof
|
|
549
|
+
// for the createLock transition `[sourceUtxo, ZERO] -> [lockedUtxo,
|
|
550
|
+
// ZERO]`. The createLock path goes through
|
|
551
|
+
// `constructPublicInputs(..., inputsLocked = false)` so we use the
|
|
552
|
+
// standard nullifier-aware encryption circuit (same as a regular
|
|
553
|
+
// unlocked transfer).
|
|
554
|
+
async function prepareCreateLockProofBytes(
|
|
555
|
+
signer: User,
|
|
556
|
+
inputs: UTXO[],
|
|
557
|
+
nullifiers: UTXO[],
|
|
558
|
+
lockedOutputs: UTXO[],
|
|
559
|
+
root: bigint,
|
|
560
|
+
merkleProofs: BigInt[][],
|
|
561
|
+
owners: User[],
|
|
562
|
+
): Promise<string> {
|
|
563
|
+
const ephemeralKeypair = genKeypair();
|
|
564
|
+
const result = await prepareProof(
|
|
565
|
+
signer,
|
|
566
|
+
inputs,
|
|
567
|
+
nullifiers,
|
|
568
|
+
lockedOutputs,
|
|
569
|
+
root as unknown as BigInt,
|
|
570
|
+
merkleProofs,
|
|
571
|
+
owners,
|
|
572
|
+
ephemeralKeypair.privKey,
|
|
573
|
+
);
|
|
574
|
+
return encodeToBytes(
|
|
575
|
+
root,
|
|
576
|
+
result.encryptionNonce,
|
|
577
|
+
ephemeralKeypair.pubKey,
|
|
578
|
+
result.encryptedValues,
|
|
579
|
+
result.encodedProof,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// prepareLockedSpendProofBytes generates a non-nullifier `anon_enc`
|
|
584
|
+
// proof for a spendLock / cancelLock settlement. The contract's
|
|
585
|
+
// locked-input verifier slot is wired to `Groth16Verifier_AnonEnc`,
|
|
586
|
+
// so we go through the same code path that {Zeto_AnonEnc} uses for
|
|
587
|
+
// its regular transfers — but wrap the result in the 5-tuple
|
|
588
|
+
// `(root, encryptionNonce, ecdhPublicKey, encryptedValues, proof)`
|
|
589
|
+
// that {Zeto_AnonEncNullifier.decodeProof_EncNullifier} expects. The
|
|
590
|
+
// `root` field is decoded but ignored when `inputsLocked = true`, so
|
|
591
|
+
// we pass 0n.
|
|
592
|
+
async function prepareLockedSpendProofBytes(
|
|
593
|
+
signer: User,
|
|
594
|
+
lockedInputs: UTXO[],
|
|
595
|
+
outputs: UTXO[],
|
|
596
|
+
owners: User[],
|
|
597
|
+
): Promise<string> {
|
|
598
|
+
const ephemeralKeypair = genKeypair();
|
|
599
|
+
const result = await prepareProofForLockedEnc(
|
|
600
|
+
circuitForLocked,
|
|
601
|
+
provingKeyForLocked,
|
|
602
|
+
signer,
|
|
603
|
+
lockedInputs,
|
|
604
|
+
outputs,
|
|
605
|
+
owners,
|
|
606
|
+
ephemeralKeypair.privKey,
|
|
607
|
+
);
|
|
608
|
+
return encodeToBytes(
|
|
609
|
+
0n,
|
|
610
|
+
result.encryptionNonce,
|
|
611
|
+
ephemeralKeypair.pubKey,
|
|
612
|
+
result.encryptedValues,
|
|
613
|
+
result.encodedProof,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
describe("createLock -> updateLock -> delegateLock -> spendLock flow", function () {
|
|
618
|
+
let bobUtxo1: UTXO;
|
|
619
|
+
let aliceUtxo1: UTXO;
|
|
620
|
+
let lockedUtxo1: UTXO;
|
|
621
|
+
let lockId: string;
|
|
622
|
+
let outputUtxo1: UTXO;
|
|
623
|
+
let outputUtxo2: UTXO;
|
|
624
|
+
let unlockHash: string;
|
|
625
|
+
|
|
626
|
+
before(async function () {
|
|
627
|
+
bobUtxo1 = newUTXO(100, Bob);
|
|
628
|
+
await doMint(zeto, deployer, [bobUtxo1]);
|
|
629
|
+
await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
630
|
+
await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
631
|
+
|
|
632
|
+
aliceUtxo1 = newUTXO(100, Alice);
|
|
633
|
+
await doMint(zeto, deployer, [aliceUtxo1]);
|
|
634
|
+
await smtAlice.add(aliceUtxo1.hash, aliceUtxo1.hash);
|
|
635
|
+
await smtBob.add(aliceUtxo1.hash, aliceUtxo1.hash);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("createLock() with deterministic lockId computed from txId", async function () {
|
|
639
|
+
const nullifier1 = newNullifier(bobUtxo1, Bob);
|
|
640
|
+
lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
|
|
641
|
+
const root = await smtBob.root();
|
|
642
|
+
const p1 = await smtBob.generateCircomVerifierProof(
|
|
643
|
+
bobUtxo1.hash,
|
|
644
|
+
root,
|
|
645
|
+
);
|
|
646
|
+
const p2 = await smtBob.generateCircomVerifierProof(0n, root);
|
|
647
|
+
const merkleProofs = [
|
|
648
|
+
p1.siblings.map((s) => s.bigInt()),
|
|
649
|
+
p2.siblings.map((s) => s.bigInt()),
|
|
650
|
+
];
|
|
651
|
+
const proofBytes = await prepareCreateLockProofBytes(
|
|
652
|
+
Bob,
|
|
653
|
+
[bobUtxo1, ZERO_UTXO],
|
|
654
|
+
[nullifier1, ZERO_UTXO],
|
|
655
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
656
|
+
root.bigInt(),
|
|
657
|
+
merkleProofs,
|
|
658
|
+
[Bob, Bob],
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const txId = randomBytes32();
|
|
662
|
+
const createArgs = encodeCreateArgs({
|
|
663
|
+
txId,
|
|
664
|
+
inputs: [nullifier1.hash],
|
|
665
|
+
outputs: [],
|
|
666
|
+
lockedOutputs: [lockedUtxo1.hash],
|
|
667
|
+
proof: proofBytes,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const predicted = await zeto
|
|
671
|
+
.connect(Bob.signer)
|
|
672
|
+
.computeLockId(createArgs);
|
|
673
|
+
lockId = predicted;
|
|
674
|
+
|
|
675
|
+
const tx = await zeto
|
|
676
|
+
.connect(Bob.signer)
|
|
677
|
+
.createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x");
|
|
678
|
+
const result: ContractTransactionReceipt | null = await tx.wait();
|
|
679
|
+
logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
|
|
680
|
+
|
|
681
|
+
const created = result!.logs
|
|
682
|
+
.map((l) => {
|
|
683
|
+
try {
|
|
684
|
+
return zeto.interface.parseLog(l as any);
|
|
685
|
+
} catch (_e) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
.find((p) => p && p.name === "LockCreated");
|
|
690
|
+
expect(created, "LockCreated event not found").to.not.be.null;
|
|
691
|
+
expect(created!.args.lockId).to.equal(predicted);
|
|
692
|
+
expect(created!.args.owner).to.equal(Bob.ethAddress);
|
|
693
|
+
expect(created!.args.spender).to.equal(Bob.ethAddress);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("isLockActive() and getLock() reflect the newly created lock", async function () {
|
|
697
|
+
expect(await zeto.isLockActive(lockId)).to.equal(true);
|
|
698
|
+
const info = await zeto.getLock(lockId);
|
|
699
|
+
expect(info.owner).to.equal(Bob.ethAddress);
|
|
700
|
+
expect(info.spender).to.equal(Bob.ethAddress);
|
|
701
|
+
expect(info.spendCommitment).to.equal(ethers.ZeroHash);
|
|
702
|
+
expect(info.cancelCommitment).to.equal(ethers.ZeroHash);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("locked() returns true for locked UTXOs and false for unlocked or spent UTXOs", async function () {
|
|
706
|
+
expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
|
|
707
|
+
true,
|
|
708
|
+
Bob.ethAddress,
|
|
709
|
+
]);
|
|
710
|
+
expect((await zeto.locked(aliceUtxo1.hash))[0]).to.be.false;
|
|
711
|
+
expect((await zeto.locked(bobUtxo1.hash))[0]).to.be.false;
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("updateLock() commits the spend hash while owner == spender", async function () {
|
|
715
|
+
outputUtxo1 = newUTXO(10, Alice);
|
|
716
|
+
outputUtxo2 = newUTXO(90, Bob);
|
|
717
|
+
|
|
718
|
+
unlockHash = calculateSpendHash(
|
|
719
|
+
[lockedUtxo1],
|
|
720
|
+
[],
|
|
721
|
+
[outputUtxo1, outputUtxo2],
|
|
722
|
+
"0x",
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
const tx = await zeto
|
|
726
|
+
.connect(Bob.signer)
|
|
727
|
+
.updateLock(
|
|
728
|
+
lockId,
|
|
729
|
+
encodeUpdateArgs(randomBytes32()),
|
|
730
|
+
unlockHash,
|
|
731
|
+
ethers.ZeroHash,
|
|
732
|
+
"0x",
|
|
733
|
+
);
|
|
734
|
+
const result: ContractTransactionReceipt | null = await tx.wait();
|
|
735
|
+
logger.debug(`updateLock() complete. Gas used: ${result?.gasUsed}`);
|
|
736
|
+
|
|
737
|
+
const info = await zeto.getLock(lockId);
|
|
738
|
+
expect(info.spendCommitment).to.equal(unlockHash);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it("delegateLock() transfers spending authority to Alice", async function () {
|
|
742
|
+
const tx = await zeto
|
|
743
|
+
.connect(Bob.signer)
|
|
744
|
+
.delegateLock(
|
|
745
|
+
lockId,
|
|
746
|
+
encodeDelegateArgs(randomBytes32()),
|
|
747
|
+
Alice.ethAddress,
|
|
748
|
+
"0x",
|
|
749
|
+
);
|
|
750
|
+
const result: ContractTransactionReceipt | null = await tx.wait();
|
|
751
|
+
logger.debug(`delegateLock() complete. Gas used: ${result?.gasUsed}`);
|
|
752
|
+
|
|
753
|
+
const info = await zeto.getLock(lockId);
|
|
754
|
+
expect(info.spender).to.equal(Alice.ethAddress);
|
|
755
|
+
expect(await zeto.locked(lockedUtxo1.hash)).to.deep.equal([
|
|
756
|
+
true,
|
|
757
|
+
Alice.ethAddress,
|
|
758
|
+
]);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("the new spender can spendLock() with the matching payload", async function () {
|
|
762
|
+
const settleProofBytes = await prepareLockedSpendProofBytes(
|
|
763
|
+
Bob,
|
|
764
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
765
|
+
[outputUtxo1, outputUtxo2],
|
|
766
|
+
[Alice, Bob],
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
const spendArgs = encodeSpendArgs({
|
|
770
|
+
txId: randomBytes32(),
|
|
771
|
+
lockedOutputs: [],
|
|
772
|
+
outputs: [outputUtxo1.hash, outputUtxo2.hash],
|
|
773
|
+
proof: settleProofBytes,
|
|
774
|
+
data: "0x",
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const tx = await zeto
|
|
778
|
+
.connect(Alice.signer)
|
|
779
|
+
.spendLock(lockId, spendArgs, "0x");
|
|
780
|
+
const result = await tx.wait();
|
|
781
|
+
|
|
782
|
+
const parsed = result!.logs
|
|
783
|
+
.map((l) => {
|
|
784
|
+
try {
|
|
785
|
+
return zeto.interface.parseLog(l as any);
|
|
786
|
+
} catch (_e) {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
.filter((p) => p !== null) as ReadonlyArray<{
|
|
791
|
+
name: string;
|
|
792
|
+
args: any;
|
|
793
|
+
}>;
|
|
794
|
+
const lockSpent = parsed.find((p) => p.name === "LockSpent");
|
|
795
|
+
const zetoLockSpent = parsed.find((p) => p.name === "ZetoLockSpent");
|
|
796
|
+
expect(lockSpent, "LockSpent event not emitted").to.not.be.undefined;
|
|
797
|
+
expect(zetoLockSpent, "ZetoLockSpent event not emitted").to.not.be
|
|
798
|
+
.undefined;
|
|
799
|
+
expect(lockSpent!.args.lockId).to.equal(lockId);
|
|
800
|
+
expect(lockSpent!.args.spender).to.equal(Alice.ethAddress);
|
|
801
|
+
|
|
802
|
+
const outputs = zetoLockSpent!.args.outputs;
|
|
803
|
+
await smtAlice.add(outputs[0], outputs[0]);
|
|
804
|
+
await smtAlice.add(outputs[1], outputs[1]);
|
|
805
|
+
await smtBob.add(outputs[0], outputs[0]);
|
|
806
|
+
await smtBob.add(outputs[1], outputs[1]);
|
|
807
|
+
|
|
808
|
+
expect(await zeto.isLockActive(lockId)).to.equal(false);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
|
|
812
|
+
const bobRoot = await smtBob.root();
|
|
813
|
+
const aliceRoot = await smtAlice.root();
|
|
814
|
+
const onchainRoot = await zeto.getRoot();
|
|
815
|
+
expect(bobRoot.string()).to.equal(onchainRoot.toString());
|
|
816
|
+
expect(aliceRoot.string()).to.equal(onchainRoot.toString());
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
describe("createLock -> cancelLock flow", function () {
|
|
821
|
+
let bobUtxo1: UTXO;
|
|
822
|
+
let lockedUtxo1: UTXO;
|
|
823
|
+
let lockId: string;
|
|
824
|
+
let cancelHash: string;
|
|
825
|
+
let outUtxo1: UTXO;
|
|
826
|
+
let outUtxo2: UTXO;
|
|
827
|
+
|
|
828
|
+
before(async function () {
|
|
829
|
+
bobUtxo1 = newUTXO(100, Bob);
|
|
830
|
+
await doMint(zeto, deployer, [bobUtxo1]);
|
|
831
|
+
await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
832
|
+
await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it("Bob createLock() with a non-zero cancelCommitment", async function () {
|
|
836
|
+
const nullifier1 = newNullifier(bobUtxo1, Bob);
|
|
837
|
+
lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
|
|
838
|
+
const root = await smtBob.root();
|
|
839
|
+
const p1 = await smtBob.generateCircomVerifierProof(
|
|
840
|
+
bobUtxo1.hash,
|
|
841
|
+
root,
|
|
842
|
+
);
|
|
843
|
+
const p2 = await smtBob.generateCircomVerifierProof(0n, root);
|
|
844
|
+
const merkleProofs = [
|
|
845
|
+
p1.siblings.map((s) => s.bigInt()),
|
|
846
|
+
p2.siblings.map((s) => s.bigInt()),
|
|
847
|
+
];
|
|
848
|
+
const proofBytes = await prepareCreateLockProofBytes(
|
|
849
|
+
Bob,
|
|
850
|
+
[bobUtxo1, ZERO_UTXO],
|
|
851
|
+
[nullifier1, ZERO_UTXO],
|
|
852
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
853
|
+
root.bigInt(),
|
|
854
|
+
merkleProofs,
|
|
855
|
+
[Bob, Bob],
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
outUtxo1 = newUTXO(10, Alice);
|
|
859
|
+
outUtxo2 = newUTXO(90, Bob);
|
|
860
|
+
cancelHash = calculateCancelHash(
|
|
861
|
+
[lockedUtxo1],
|
|
862
|
+
[],
|
|
863
|
+
[outUtxo1, outUtxo2],
|
|
864
|
+
"0x",
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
const createArgs = encodeCreateArgs({
|
|
868
|
+
txId: randomBytes32(),
|
|
869
|
+
inputs: [nullifier1.hash],
|
|
870
|
+
outputs: [],
|
|
871
|
+
lockedOutputs: [lockedUtxo1.hash],
|
|
872
|
+
proof: proofBytes,
|
|
873
|
+
});
|
|
874
|
+
lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
|
|
875
|
+
const tx = await zeto
|
|
876
|
+
.connect(Bob.signer)
|
|
877
|
+
.createLock(createArgs, ethers.ZeroHash, cancelHash, "0x");
|
|
878
|
+
const result: ContractTransactionReceipt | null = await tx.wait();
|
|
879
|
+
logger.debug(`createLock() complete. Gas used: ${result?.gasUsed}`);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it("the owner can cancelLock() to reverse the lock without delegation", async function () {
|
|
883
|
+
const cancelProofBytes = await prepareLockedSpendProofBytes(
|
|
884
|
+
Bob,
|
|
885
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
886
|
+
[outUtxo1, outUtxo2],
|
|
887
|
+
[Alice, Bob],
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
const cancelArgs = encodeSpendArgs({
|
|
891
|
+
txId: randomBytes32(),
|
|
892
|
+
lockedOutputs: [],
|
|
893
|
+
outputs: [outUtxo1.hash, outUtxo2.hash],
|
|
894
|
+
proof: cancelProofBytes,
|
|
895
|
+
data: "0x",
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
const tx = await zeto
|
|
899
|
+
.connect(Bob.signer)
|
|
900
|
+
.cancelLock(lockId, cancelArgs, "0x");
|
|
901
|
+
const result = await tx.wait();
|
|
902
|
+
|
|
903
|
+
const parsed = result!.logs
|
|
904
|
+
.map((l) => {
|
|
905
|
+
try {
|
|
906
|
+
return zeto.interface.parseLog(l as any);
|
|
907
|
+
} catch (_e) {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
})
|
|
911
|
+
.filter((p) => p !== null) as ReadonlyArray<{
|
|
912
|
+
name: string;
|
|
913
|
+
args: any;
|
|
914
|
+
}>;
|
|
915
|
+
const cancelled = parsed.find((p) => p.name === "LockCancelled");
|
|
916
|
+
const zetoCancelled = parsed.find(
|
|
917
|
+
(p) => p.name === "ZetoLockCancelled",
|
|
918
|
+
);
|
|
919
|
+
expect(cancelled, "LockCancelled event not emitted").to.not.be
|
|
920
|
+
.undefined;
|
|
921
|
+
expect(zetoCancelled, "ZetoLockCancelled event not emitted").to.not.be
|
|
922
|
+
.undefined;
|
|
923
|
+
|
|
924
|
+
const outputs = zetoCancelled!.args.outputs;
|
|
925
|
+
await smtAlice.add(outputs[0], outputs[0]);
|
|
926
|
+
await smtAlice.add(outputs[1], outputs[1]);
|
|
927
|
+
await smtBob.add(outputs[0], outputs[0]);
|
|
928
|
+
await smtBob.add(outputs[1], outputs[1]);
|
|
929
|
+
|
|
930
|
+
expect(await zeto.isLockActive(lockId)).to.equal(false);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it("onchain SMT root for the unlocked UTXOs equals the offchain SMT root", async function () {
|
|
934
|
+
const bobRoot = await smtBob.root();
|
|
935
|
+
const aliceRoot = await smtAlice.root();
|
|
936
|
+
const onchainRoot = await zeto.getRoot();
|
|
937
|
+
expect(bobRoot.string()).to.equal(onchainRoot.toString());
|
|
938
|
+
expect(aliceRoot.string()).to.equal(onchainRoot.toString());
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
describe("spendLock with a payload that does not match the spend commitment fails", function () {
|
|
943
|
+
let bobUtxo1: UTXO;
|
|
944
|
+
let lockedUtxo1: UTXO;
|
|
945
|
+
let lockId: string;
|
|
946
|
+
let expectedHash: string;
|
|
947
|
+
|
|
948
|
+
before(async function () {
|
|
949
|
+
bobUtxo1 = newUTXO(100, Bob);
|
|
950
|
+
await doMint(zeto, deployer, [bobUtxo1]);
|
|
951
|
+
await smtAlice.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
952
|
+
await smtBob.add(bobUtxo1.hash, bobUtxo1.hash);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it("Bob createLock() then updateLock() committing a specific spend hash", async function () {
|
|
956
|
+
const nullifier1 = newNullifier(bobUtxo1, Bob);
|
|
957
|
+
lockedUtxo1 = newUTXO(bobUtxo1.value!, Bob);
|
|
958
|
+
const root = await smtBob.root();
|
|
959
|
+
const p1 = await smtBob.generateCircomVerifierProof(
|
|
960
|
+
bobUtxo1.hash,
|
|
961
|
+
root,
|
|
962
|
+
);
|
|
963
|
+
const p2 = await smtBob.generateCircomVerifierProof(0n, root);
|
|
964
|
+
const merkleProofs = [
|
|
965
|
+
p1.siblings.map((s) => s.bigInt()),
|
|
966
|
+
p2.siblings.map((s) => s.bigInt()),
|
|
967
|
+
];
|
|
968
|
+
const proofBytes = await prepareCreateLockProofBytes(
|
|
969
|
+
Bob,
|
|
970
|
+
[bobUtxo1, ZERO_UTXO],
|
|
971
|
+
[nullifier1, ZERO_UTXO],
|
|
972
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
973
|
+
root.bigInt(),
|
|
974
|
+
merkleProofs,
|
|
975
|
+
[Bob, Bob],
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
const createArgs = encodeCreateArgs({
|
|
979
|
+
txId: randomBytes32(),
|
|
980
|
+
inputs: [nullifier1.hash],
|
|
981
|
+
outputs: [],
|
|
982
|
+
lockedOutputs: [lockedUtxo1.hash],
|
|
983
|
+
proof: proofBytes,
|
|
984
|
+
});
|
|
985
|
+
lockId = await zeto.connect(Bob.signer).computeLockId(createArgs);
|
|
986
|
+
await (
|
|
987
|
+
await zeto
|
|
988
|
+
.connect(Bob.signer)
|
|
989
|
+
.createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x")
|
|
990
|
+
).wait();
|
|
991
|
+
|
|
992
|
+
const expectedOut1 = newUTXO(10, Alice);
|
|
993
|
+
const expectedOut2 = newUTXO(90, Bob);
|
|
994
|
+
expectedHash = calculateSpendHash(
|
|
995
|
+
[lockedUtxo1],
|
|
996
|
+
[],
|
|
997
|
+
[expectedOut1, expectedOut2],
|
|
998
|
+
"0x",
|
|
999
|
+
);
|
|
1000
|
+
await (
|
|
1001
|
+
await zeto
|
|
1002
|
+
.connect(Bob.signer)
|
|
1003
|
+
.updateLock(
|
|
1004
|
+
lockId,
|
|
1005
|
+
encodeUpdateArgs(randomBytes32()),
|
|
1006
|
+
expectedHash,
|
|
1007
|
+
ethers.ZeroHash,
|
|
1008
|
+
"0x",
|
|
1009
|
+
)
|
|
1010
|
+
).wait();
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it("spendLock() with a different payload reverts with InvalidUnlockHash", async function () {
|
|
1014
|
+
if (network.name !== "hardhat") {
|
|
1015
|
+
this.skip();
|
|
1016
|
+
}
|
|
1017
|
+
const wrongOut1 = newUTXO(20, Alice);
|
|
1018
|
+
const wrongOut2 = newUTXO(80, Bob);
|
|
1019
|
+
|
|
1020
|
+
const settleProofBytes = await prepareLockedSpendProofBytes(
|
|
1021
|
+
Bob,
|
|
1022
|
+
[lockedUtxo1, ZERO_UTXO],
|
|
1023
|
+
[wrongOut1, wrongOut2],
|
|
1024
|
+
[Alice, Bob],
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
const spendArgs = encodeSpendArgs({
|
|
1028
|
+
txId: randomBytes32(),
|
|
1029
|
+
lockedOutputs: [],
|
|
1030
|
+
outputs: [wrongOut1.hash, wrongOut2.hash],
|
|
1031
|
+
proof: settleProofBytes,
|
|
1032
|
+
data: "0x",
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
const calculatedHash = calculateSpendHash(
|
|
1036
|
+
[lockedUtxo1],
|
|
1037
|
+
[],
|
|
1038
|
+
[wrongOut1, wrongOut2],
|
|
1039
|
+
"0x",
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
await expect(
|
|
1043
|
+
zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x"),
|
|
1044
|
+
)
|
|
1045
|
+
.to.be.revertedWithCustomError(zeto, "InvalidUnlockHash")
|
|
1046
|
+
.withArgs(expectedHash, calculatedHash);
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
describe("negative cases for the lock lifecycle", function () {
|
|
1051
|
+
// These tests rely on hardhat-style revert decoding.
|
|
1052
|
+
if (network.name !== "hardhat") {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// freshLock mints a UTXO for `owner`, locks it, and returns the
|
|
1057
|
+
// resulting lock metadata so each negative case can branch off
|
|
1058
|
+
// without polluting other test scopes. Each call advances both
|
|
1059
|
+
// local SMTs to keep them in sync with the on-chain unlocked-UTXO
|
|
1060
|
+
// tree (the createLock proof needs a valid SMT inclusion proof).
|
|
1061
|
+
async function freshLock(
|
|
1062
|
+
owner: User,
|
|
1063
|
+
spendCommitment: string = ethers.ZeroHash,
|
|
1064
|
+
cancelCommitment: string = ethers.ZeroHash,
|
|
1065
|
+
): Promise<{
|
|
1066
|
+
lockId: string;
|
|
1067
|
+
sourceUtxo: UTXO;
|
|
1068
|
+
lockedUtxo: UTXO;
|
|
1069
|
+
createArgs: string;
|
|
1070
|
+
}> {
|
|
1071
|
+
const sourceUtxo = newUTXO(100, owner);
|
|
1072
|
+
await doMint(zeto, deployer, [sourceUtxo]);
|
|
1073
|
+
await smtAlice.add(sourceUtxo.hash, sourceUtxo.hash);
|
|
1074
|
+
await smtBob.add(sourceUtxo.hash, sourceUtxo.hash);
|
|
1075
|
+
|
|
1076
|
+
const nullifier = newNullifier(sourceUtxo, owner);
|
|
1077
|
+
const lockedUtxo = newUTXO(sourceUtxo.value!, owner);
|
|
1078
|
+
const root = await smtBob.root();
|
|
1079
|
+
const p1 = await smtBob.generateCircomVerifierProof(
|
|
1080
|
+
sourceUtxo.hash,
|
|
1081
|
+
root,
|
|
1082
|
+
);
|
|
1083
|
+
const p2 = await smtBob.generateCircomVerifierProof(0n, root);
|
|
1084
|
+
const merkleProofs = [
|
|
1085
|
+
p1.siblings.map((s) => s.bigInt()),
|
|
1086
|
+
p2.siblings.map((s) => s.bigInt()),
|
|
1087
|
+
];
|
|
1088
|
+
const proofBytes = await prepareCreateLockProofBytes(
|
|
1089
|
+
owner,
|
|
1090
|
+
[sourceUtxo, ZERO_UTXO],
|
|
1091
|
+
[nullifier, ZERO_UTXO],
|
|
1092
|
+
[lockedUtxo, ZERO_UTXO],
|
|
1093
|
+
root.bigInt(),
|
|
1094
|
+
merkleProofs,
|
|
1095
|
+
[owner, owner],
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
const createArgs = encodeCreateArgs({
|
|
1099
|
+
txId: randomBytes32(),
|
|
1100
|
+
inputs: [nullifier.hash],
|
|
1101
|
+
outputs: [],
|
|
1102
|
+
lockedOutputs: [lockedUtxo.hash],
|
|
1103
|
+
proof: proofBytes,
|
|
1104
|
+
});
|
|
1105
|
+
const lockId = await zeto
|
|
1106
|
+
.connect(owner.signer)
|
|
1107
|
+
.computeLockId(createArgs);
|
|
1108
|
+
await (
|
|
1109
|
+
await zeto
|
|
1110
|
+
.connect(owner.signer)
|
|
1111
|
+
.createLock(createArgs, spendCommitment, cancelCommitment, "0x")
|
|
1112
|
+
).wait();
|
|
1113
|
+
return { lockId, sourceUtxo, lockedUtxo, createArgs };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function dummySpendArgs(): string {
|
|
1117
|
+
return encodeSpendArgs({
|
|
1118
|
+
txId: randomBytes32(),
|
|
1119
|
+
lockedOutputs: [],
|
|
1120
|
+
outputs: [],
|
|
1121
|
+
proof: "0x",
|
|
1122
|
+
data: "0x",
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
it("createLock() with a duplicate txId from the same caller reverts with DuplicateLock", async function () {
|
|
1127
|
+
const { lockId, createArgs } = await freshLock(Bob);
|
|
1128
|
+
|
|
1129
|
+
await expect(
|
|
1130
|
+
zeto
|
|
1131
|
+
.connect(Bob.signer)
|
|
1132
|
+
.createLock(createArgs, ethers.ZeroHash, ethers.ZeroHash, "0x"),
|
|
1133
|
+
)
|
|
1134
|
+
.to.be.revertedWithCustomError(zeto, "DuplicateLock")
|
|
1135
|
+
.withArgs(lockId);
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it("updateLock() by a non-owner reverts with LockUnauthorized", async function () {
|
|
1139
|
+
const { lockId } = await freshLock(Bob);
|
|
1140
|
+
|
|
1141
|
+
await expect(
|
|
1142
|
+
zeto
|
|
1143
|
+
.connect(Alice.signer)
|
|
1144
|
+
.updateLock(
|
|
1145
|
+
lockId,
|
|
1146
|
+
encodeUpdateArgs(randomBytes32()),
|
|
1147
|
+
ethers.ZeroHash,
|
|
1148
|
+
ethers.ZeroHash,
|
|
1149
|
+
"0x",
|
|
1150
|
+
),
|
|
1151
|
+
)
|
|
1152
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1153
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it("updateLock() after delegateLock() reverts with LockImmutable", async function () {
|
|
1157
|
+
const { lockId } = await freshLock(Bob);
|
|
1158
|
+
|
|
1159
|
+
await (
|
|
1160
|
+
await zeto
|
|
1161
|
+
.connect(Bob.signer)
|
|
1162
|
+
.delegateLock(
|
|
1163
|
+
lockId,
|
|
1164
|
+
encodeDelegateArgs(randomBytes32()),
|
|
1165
|
+
Alice.ethAddress,
|
|
1166
|
+
"0x",
|
|
1167
|
+
)
|
|
1168
|
+
).wait();
|
|
1169
|
+
|
|
1170
|
+
await expect(
|
|
1171
|
+
zeto
|
|
1172
|
+
.connect(Bob.signer)
|
|
1173
|
+
.updateLock(
|
|
1174
|
+
lockId,
|
|
1175
|
+
encodeUpdateArgs(randomBytes32()),
|
|
1176
|
+
ethers.ZeroHash,
|
|
1177
|
+
ethers.ZeroHash,
|
|
1178
|
+
"0x",
|
|
1179
|
+
),
|
|
1180
|
+
)
|
|
1181
|
+
.to.be.revertedWithCustomError(zeto, "LockImmutable")
|
|
1182
|
+
.withArgs(lockId);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it("delegateLock() by a non-spender reverts with LockUnauthorized", async function () {
|
|
1186
|
+
const { lockId } = await freshLock(Bob);
|
|
1187
|
+
|
|
1188
|
+
await expect(
|
|
1189
|
+
zeto
|
|
1190
|
+
.connect(Alice.signer)
|
|
1191
|
+
.delegateLock(
|
|
1192
|
+
lockId,
|
|
1193
|
+
encodeDelegateArgs(randomBytes32()),
|
|
1194
|
+
Charlie.ethAddress,
|
|
1195
|
+
"0x",
|
|
1196
|
+
),
|
|
1197
|
+
)
|
|
1198
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1199
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it("spendLock() by a non-spender reverts with LockUnauthorized before touching the proof", async function () {
|
|
1203
|
+
const { lockId } = await freshLock(Bob);
|
|
1204
|
+
|
|
1205
|
+
await expect(
|
|
1206
|
+
zeto.connect(Alice.signer).spendLock(lockId, dummySpendArgs(), "0x"),
|
|
1207
|
+
)
|
|
1208
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1209
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
it("cancelLock() by a non-spender reverts with LockUnauthorized", async function () {
|
|
1213
|
+
const { lockId } = await freshLock(Bob);
|
|
1214
|
+
|
|
1215
|
+
await expect(
|
|
1216
|
+
zeto.connect(Alice.signer).cancelLock(lockId, dummySpendArgs(), "0x"),
|
|
1217
|
+
)
|
|
1218
|
+
.to.be.revertedWithCustomError(zeto, "LockUnauthorized")
|
|
1219
|
+
.withArgs(lockId, Bob.ethAddress, Alice.ethAddress);
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
it("after a successful spendLock(), the lock is no longer active and getLock() reverts", async function () {
|
|
1223
|
+
const { lockId, lockedUtxo } = await freshLock(Bob);
|
|
1224
|
+
|
|
1225
|
+
const out1 = newUTXO(10, Alice);
|
|
1226
|
+
const out2 = newUTXO(90, Bob);
|
|
1227
|
+
const settleProofBytes = await prepareLockedSpendProofBytes(
|
|
1228
|
+
Bob,
|
|
1229
|
+
[lockedUtxo, ZERO_UTXO],
|
|
1230
|
+
[out1, out2],
|
|
1231
|
+
[Alice, Bob],
|
|
1232
|
+
);
|
|
1233
|
+
const spendArgs = encodeSpendArgs({
|
|
1234
|
+
txId: randomBytes32(),
|
|
1235
|
+
lockedOutputs: [],
|
|
1236
|
+
outputs: [out1.hash, out2.hash],
|
|
1237
|
+
proof: settleProofBytes,
|
|
1238
|
+
data: "0x",
|
|
1239
|
+
});
|
|
1240
|
+
await (
|
|
1241
|
+
await zeto.connect(Bob.signer).spendLock(lockId, spendArgs, "0x")
|
|
1242
|
+
).wait();
|
|
1243
|
+
// Keep both SMTs in sync with the new unlocked outputs so
|
|
1244
|
+
// unrelated tests in later blocks can still reason about roots.
|
|
1245
|
+
await smtAlice.add(out1.hash, out1.hash);
|
|
1246
|
+
await smtAlice.add(out2.hash, out2.hash);
|
|
1247
|
+
await smtBob.add(out1.hash, out1.hash);
|
|
1248
|
+
await smtBob.add(out2.hash, out2.hash);
|
|
1249
|
+
|
|
1250
|
+
expect(await zeto.isLockActive(lockId)).to.equal(false);
|
|
1251
|
+
await expect(zeto.getLock(lockId))
|
|
1252
|
+
.to.be.revertedWithCustomError(zeto, "LockNotActive")
|
|
1253
|
+
.withArgs(lockId);
|
|
1254
|
+
// Re-spending a consumed lock MUST also fail — lockActive is the
|
|
1255
|
+
// first modifier and emits LockNotActive before onlySpender.
|
|
1256
|
+
await expect(
|
|
1257
|
+
zeto.connect(Bob.signer).spendLock(lockId, dummySpendArgs(), "0x"),
|
|
1258
|
+
)
|
|
1259
|
+
.to.be.revertedWithCustomError(zeto, "LockNotActive")
|
|
1260
|
+
.withArgs(lockId);
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
// describe("legacy lockStates() tests, kept for reference", function () {
|
|
477
1266
|
// let nullifier1: any;
|
|
478
1267
|
// it("lockStates() should succeed when using unlocked states", async function () {
|
|
479
1268
|
// nullifier1 = newNullifier(utxo4, Alice);
|
|
@@ -651,8 +1440,7 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
651
1440
|
10,
|
|
652
1441
|
nullifiers,
|
|
653
1442
|
outputCommitments[0],
|
|
654
|
-
root.bigInt(),
|
|
655
|
-
encodedProof,
|
|
1443
|
+
encodeToBytesForWithdraw(root.bigInt(), encodedProof),
|
|
656
1444
|
"0x",
|
|
657
1445
|
),
|
|
658
1446
|
).rejectedWith("UTXOAlreadySpent");
|
|
@@ -970,11 +1758,13 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
970
1758
|
const tx = await zeto.connect(signer.signer).transfer(
|
|
971
1759
|
nullifiers.filter((ic) => ic !== 0n), // trim off empty utxo hashes to check padding logic for batching works
|
|
972
1760
|
outputCommitments.filter((oc) => oc !== 0n), // trim off empty utxo hashes to check padding logic for batching works
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1761
|
+
encodeToBytes(
|
|
1762
|
+
root,
|
|
1763
|
+
encryptionNonce,
|
|
1764
|
+
ecdhPublicKey,
|
|
1765
|
+
encryptedValues,
|
|
1766
|
+
encodedProof,
|
|
1767
|
+
),
|
|
978
1768
|
"0x",
|
|
979
1769
|
);
|
|
980
1770
|
const results: ContractTransactionReceipt | null = await tx.wait();
|
|
@@ -984,3 +1774,22 @@ describe("Zeto based fungible token with anonymity using nullifiers and encrypti
|
|
|
984
1774
|
return results;
|
|
985
1775
|
}
|
|
986
1776
|
});
|
|
1777
|
+
|
|
1778
|
+
function encodeToBytes(
|
|
1779
|
+
root: any,
|
|
1780
|
+
encryptionNonce: any,
|
|
1781
|
+
ecdhPublicKey: any,
|
|
1782
|
+
encryptedValues: any,
|
|
1783
|
+
proof: any,
|
|
1784
|
+
) {
|
|
1785
|
+
return new AbiCoder().encode(
|
|
1786
|
+
[
|
|
1787
|
+
"uint256 root",
|
|
1788
|
+
"uint256 encryptionNonce",
|
|
1789
|
+
"uint256[2] ecdhPublicKey",
|
|
1790
|
+
"uint256[] encryptedValues",
|
|
1791
|
+
"tuple(uint256[2] pA, uint256[2][2] pB, uint256[2] pC)",
|
|
1792
|
+
],
|
|
1793
|
+
[root, encryptionNonce, ecdhPublicKey, encryptedValues, proof],
|
|
1794
|
+
);
|
|
1795
|
+
}
|