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