@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.
- package/README.md +76 -162
- package/config/eip170.ts +30 -0
- package/contracts/factory.sol +30 -34
- package/contracts/factory_upgradeable.sol +11 -7
- 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/IZetoLockHooks.sol +42 -0
- 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 +74 -26
- 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 +443 -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 +219 -0
- package/contracts/lib/zeto_lockable_lib.sol +460 -0
- package/contracts/lib/zeto_lockable_storage.sol +43 -0
- package/contracts/lib/zeto_non_fungible.sol +228 -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 +6 -1
- 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 +217 -361
- package/contracts/zeto_nf_anon.sol +55 -130
- package/contracts/zeto_nf_anon_nullifier.sol +90 -152
- package/hardhat.config.ts +18 -3
- package/ignition/modules/lib/deps.ts +13 -0
- package/ignition/modules/test/tendecimals.ts +7 -1
- package/ignition/modules/zeto_anon.ts +3 -0
- package/ignition/modules/zeto_anon_burnable.ts +11 -7
- package/ignition/modules/zeto_anon_enc.ts +3 -0
- package/ignition/modules/zeto_anon_enc_nullifier.ts +34 -0
- package/ignition/modules/zeto_anon_enc_nullifier_kyc.ts +5 -2
- package/ignition/modules/zeto_anon_enc_nullifier_non_repudiation.ts +3 -0
- package/ignition/modules/zeto_anon_nullifier.ts +31 -4
- package/ignition/modules/zeto_anon_nullifier_burnable.ts +53 -16
- package/ignition/modules/zeto_anon_nullifier_kyc.ts +30 -12
- package/ignition/modules/zeto_anon_nullifier_qurrency.ts +3 -0
- package/ignition/modules/zeto_nf_anon.ts +3 -1
- package/ignition/modules/zeto_nf_anon_nullifier.ts +17 -12
- package/package.json +7 -3
- package/scripts/deploy_cloneable.ts +19 -10
- package/scripts/deploy_upgradeable.ts +9 -5
- package/scripts/lib/zeto_libraries.ts +47 -0
- package/scripts/tokens/Zeto_Anon.ts +6 -3
- package/scripts/tokens/Zeto_AnonBurnable.ts +4 -1
- package/scripts/tokens/Zeto_AnonEnc.ts +15 -3
- package/scripts/tokens/Zeto_AnonEncNullifier.ts +11 -8
- package/scripts/tokens/Zeto_AnonEncNullifierKyc.ts +7 -6
- package/scripts/tokens/Zeto_AnonEncNullifierNonRepudiation.ts +7 -6
- package/scripts/tokens/Zeto_AnonNullifier.ts +7 -6
- package/scripts/tokens/Zeto_AnonNullifierBurnable.ts +7 -6
- package/scripts/tokens/Zeto_AnonNullifierKyc.ts +7 -6
- package/scripts/tokens/Zeto_AnonNullifierQurrency.ts +7 -8
- package/scripts/tokens/Zeto_NfAnon.ts +5 -3
- package/scripts/tokens/Zeto_NfAnonNullifier.ts +7 -7
- package/test/factory.ts +55 -73
- package/test/lib/anon_nullifier_helpers.ts +89 -0
- package/test/lib/anon_zeto_helpers.ts +76 -0
- package/test/lib/deploy.ts +5 -2
- package/test/lib/eip170.ts +23 -0
- 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 -27
- package/test/utils.ts +144 -21
- package/test/zeto_anon.ts +956 -465
- package/test/zeto_anon_enc.ts +881 -143
- package/test/zeto_anon_enc_nullifier.ts +850 -39
- package/test/zeto_anon_enc_nullifier_kyc.ts +123 -182
- package/test/zeto_anon_enc_nullifier_non_repudiation.ts +80 -47
- package/test/zeto_anon_nullifier.ts +1212 -954
- package/test/zeto_anon_nullifier_kyc.ts +677 -656
- package/test/zeto_anon_nullifier_qurrency.ts +455 -307
- package/test/zeto_nf_anon.ts +737 -138
- package/test/zeto_nf_anon_nullifier.ts +876 -247
- 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 -340
- package/contracts/zkDvP.sol_ +0 -273
- package/test/zkDvP.ts_ +0 -455
- /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 {
|
|
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 {
|
|
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("
|
|
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
|
|
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
|
-
|
|
414
|
-
"http://ipfs.io/
|
|
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
|
-
|
|
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
|
-
|
|
523
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
539
|
-
`
|
|
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(
|
|
1166
|
+
.transfer(
|
|
1167
|
+
nullifier,
|
|
1168
|
+
outputCommitment,
|
|
1169
|
+
encodeUnlockedProof(root, encodedProof),
|
|
1170
|
+
"0x",
|
|
1171
|
+
);
|
|
561
1172
|
const results: ContractTransactionReceipt | null = await tx.wait();
|
|
562
|
-
|
|
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
|
+
}
|