Merge 19-add-anvil-e2e-test into main
2d802ed parent: 48af163 modified
CHANGELOG.md +8 -0 | @@ -89,6 +89,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||
| 89 | 89 | |
| 90 | 90 | ### Added |
| 91 | 91 | |
| 92 | +- `zk/scripts/e2e.mjs`, run with `pnpm e2e` in `zk/`: brings up a local anvil, | |
| 93 | + deploys LOVE with `script/Love.s.sol`, deposits three notes, rebuilds the tree | |
| 94 | + from the `Deposit` events, proves with `zk/lib.mjs` and claims from an | |
| 95 | + unrelated address, then checks an old root still works and that a spent note | |
| 96 | + and a claim paid to someone else are refused. It follows `docs/ZK.md`, so it | |
| 97 | + is also what checks the snippets there. `zk/` gains `ethers` v6 for it. | |
| 98 | +- `zk/.npmrc`, setting `manage-package-manager-versions=false` so pnpm stops | |
| 99 | + writing a `packageManager` field into `zk/package.json`. | |
| 92 | 100 | - `zk/`: the `withdraw` circom circuit (Poseidon Merkle membership at depth 10, |
| 93 | 101 | a nullifier, and the recipient bound to the proof, about 6,100 constraints), |
| 94 | 102 | `pnpm setup` for the Groth16 trusted setup, and `zk/lib.mjs` for making a |
| @@ -89,6 +89,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| 89 | 89 | ||
| 90 | ### Added | 90 | ### Added |
| 91 | 91 | ||
| 92 | +- `zk/scripts/e2e.mjs`, run with `pnpm e2e` in `zk/`: brings up a local anvil, | ||
| 93 | + deploys LOVE with `script/Love.s.sol`, deposits three notes, rebuilds the tree | ||
| 94 | + from the `Deposit` events, proves with `zk/lib.mjs` and claims from an | ||
| 95 | + unrelated address, then checks an old root still works and that a spent note | ||
| 96 | + and a claim paid to someone else are refused. It follows `docs/ZK.md`, so it | ||
| 97 | + is also what checks the snippets there. `zk/` gains `ethers` v6 for it. | ||
| 98 | +- `zk/.npmrc`, setting `manage-package-manager-versions=false` so pnpm stops | ||
| 99 | + writing a `packageManager` field into `zk/package.json`. | ||
| 92 | - `zk/`: the `withdraw` circom circuit (Poseidon Merkle membership at depth 10, | 100 | - `zk/`: the `withdraw` circom circuit (Poseidon Merkle membership at depth 10, |
| 93 | a nullifier, and the recipient bound to the proof, about 6,100 constraints), | 101 | a nullifier, and the recipient bound to the proof, about 6,100 constraints), |
| 94 | `pnpm setup` for the Groth16 trusted setup, and `zk/lib.mjs` for making a | 102 | `pnpm setup` for the Groth16 trusted setup, and `zk/lib.mjs` for making a |
modified
README.md +6 -0 | @@ -21,6 +21,12 @@ forge build | ||
| 21 | 21 | forge test |
| 22 | 22 | ``` |
| 23 | 23 | |
| 24 | +The private pool has an end-to-end run against a local anvil, which needs `anvil` and `forge` on your `PATH`: | |
| 25 | + | |
| 26 | +```shell | |
| 27 | +cd zk && pnpm install && pnpm e2e | |
| 28 | +``` | |
| 29 | + | |
| 24 | 30 | ## Deploy |
| 25 | 31 | |
| 26 | 32 | Deployment uses CREATE2, so the token has the same address on every EVM network. |
| @@ -21,6 +21,12 @@ forge build | |||
| 21 | forge test | 21 | forge test |
| 22 | ``` | 22 | ``` |
| 23 | 23 | ||
| 24 | +The private pool has an end-to-end run against a local anvil, which needs `anvil` and `forge` on your `PATH`: | ||
| 25 | + | ||
| 26 | +```shell | ||
| 27 | +cd zk && pnpm install && pnpm e2e | ||
| 28 | +``` | ||
| 29 | + | ||
| 24 | ## Deploy | 30 | ## Deploy |
| 25 | 31 | ||
| 26 | Deployment uses CREATE2, so the token has the same address on every EVM network. | 32 | Deployment uses CREATE2, so the token has the same address on every EVM network. |
modified
docs/ZK.md +12 -0 | @@ -118,6 +118,18 @@ This is fine for a testnet. Before it holds real value: | ||
| 118 | 118 | - Run phase 2 with several independent contributors. |
| 119 | 119 | - Have the circuit and the vendored Poseidon audited. |
| 120 | 120 | |
| 121 | +## Testing | |
| 122 | + | |
| 123 | +`forge test` covers the contracts against the proofs in `test/fixtures/proofs.json`, which are replayed, so it never runs the prover. `pnpm e2e` does: it needs [anvil](https://book.getfoundry.sh/anvil/) and `forge` on your `PATH`, and runs the whole cycle on a local chain. | |
| 124 | + | |
| 125 | +```shell | |
| 126 | +cd zk | |
| 127 | +pnpm install | |
| 128 | +pnpm e2e | |
| 129 | +``` | |
| 130 | + | |
| 131 | +It starts anvil as Base (chain 8453), puts Base's wETH at the OP Stack predeploy address, and deploys with [`script/Love.s.sol`](../script/Love.s.sol). It then deposits three notes, rebuilds the tree from the events, proves with `zk/lib.mjs` and claims to a fresh address from an unrelated one. It also claims against an older root, and checks that a spent note and a claim paid to someone else both revert. Set `E2E_PORT` if 8599 is taken. Nothing leaves your machine. | |
| 132 | + | |
| 121 | 133 | ## Limits |
| 122 | 134 | |
| 123 | 135 | - **Small anonymity set.** It starts at one and grows only with deposits. Below a few dozen, treat a claim as weakly private. |
| @@ -118,6 +118,18 @@ This is fine for a testnet. Before it holds real value: | |||
| 118 | - Run phase 2 with several independent contributors. | 118 | - Run phase 2 with several independent contributors. |
| 119 | - Have the circuit and the vendored Poseidon audited. | 119 | - Have the circuit and the vendored Poseidon audited. |
| 120 | 120 | ||
| 121 | +## Testing | ||
| 122 | + | ||
| 123 | +`forge test` covers the contracts against the proofs in `test/fixtures/proofs.json`, which are replayed, so it never runs the prover. `pnpm e2e` does: it needs [anvil](https://book.getfoundry.sh/anvil/) and `forge` on your `PATH`, and runs the whole cycle on a local chain. | ||
| 124 | + | ||
| 125 | +```shell | ||
| 126 | +cd zk | ||
| 127 | +pnpm install | ||
| 128 | +pnpm e2e | ||
| 129 | +``` | ||
| 130 | + | ||
| 131 | +It starts anvil as Base (chain 8453), puts Base's wETH at the OP Stack predeploy address, and deploys with [`script/Love.s.sol`](../script/Love.s.sol). It then deposits three notes, rebuilds the tree from the events, proves with `zk/lib.mjs` and claims to a fresh address from an unrelated one. It also claims against an older root, and checks that a spent note and a claim paid to someone else both revert. Set `E2E_PORT` if 8599 is taken. Nothing leaves your machine. | ||
| 132 | + | ||
| 121 | ## Limits | 133 | ## Limits |
| 122 | 134 | ||
| 123 | - **Small anonymity set.** It starts at one and grows only with deposits. Below a few dozen, treat a claim as weakly private. | 135 | - **Small anonymity set.** It starts at one and grows only with deposits. Below a few dozen, treat a claim as weakly private. |
added
zk/.npmrc +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +manage-package-manager-versions=false | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +manage-package-manager-versions=false | ||
modified
zk/package.json +3 -1 | @@ -6,11 +6,13 @@ | ||
| 6 | 6 | "scripts": { |
| 7 | 7 | "compile": "circom circuits/withdraw.circom --r1cs --wasm -l node_modules -o build/tmp", |
| 8 | 8 | "setup": "./scripts/setup.sh", |
| 9 | - "fixture": "node scripts/fixture.mjs" | |
| 9 | + "fixture": "node scripts/fixture.mjs", | |
| 10 | + "e2e": "node scripts/e2e.mjs" | |
| 10 | 11 | }, |
| 11 | 12 | "dependencies": { |
| 12 | 13 | "circomlib": "^2.0.5", |
| 13 | 14 | "circomlibjs": "^0.1.7", |
| 15 | + "ethers": "^6.17.0", | |
| 14 | 16 | "snarkjs": "^0.7.6" |
| 15 | 17 | } |
| 16 | 18 | } |
| @@ -6,11 +6,13 @@ | |||
| 6 | "scripts": { | 6 | "scripts": { |
| 7 | "compile": "circom circuits/withdraw.circom --r1cs --wasm -l node_modules -o build/tmp", | 7 | "compile": "circom circuits/withdraw.circom --r1cs --wasm -l node_modules -o build/tmp", |
| 8 | "setup": "./scripts/setup.sh", | 8 | "setup": "./scripts/setup.sh", |
| 9 | - "fixture": "node scripts/fixture.mjs" | 9 | + "fixture": "node scripts/fixture.mjs", |
| 10 | + "e2e": "node scripts/e2e.mjs" | ||
| 10 | }, | 11 | }, |
| 11 | "dependencies": { | 12 | "dependencies": { |
| 12 | "circomlib": "^2.0.5", | 13 | "circomlib": "^2.0.5", |
| 13 | "circomlibjs": "^0.1.7", | 14 | "circomlibjs": "^0.1.7", |
| 15 | + "ethers": "^6.17.0", | ||
| 14 | "snarkjs": "^0.7.6" | 16 | "snarkjs": "^0.7.6" |
| 15 | } | 17 | } |
| 16 | } | 18 | } |
modified
zk/pnpm-lock.yaml +74 -0 | @@ -14,12 +14,18 @@ importers: | ||
| 14 | 14 | circomlibjs: |
| 15 | 15 | specifier: ^0.1.7 |
| 16 | 16 | version: 0.1.7 |
| 17 | + ethers: | |
| 18 | + specifier: ^6.17.0 | |
| 19 | + version: 6.17.0 | |
| 17 | 20 | snarkjs: |
| 18 | 21 | specifier: ^0.7.6 |
| 19 | 22 | version: 0.7.6 |
| 20 | 23 | |
| 21 | 24 | packages: |
| 22 | 25 | |
| 26 | + '@adraffy/ens-normalize@1.11.1': | |
| 27 | + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} | |
| 28 | + | |
| 23 | 29 | '@ethersproject/abi@5.8.0': |
| 24 | 30 | resolution: {integrity: sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==} |
| 25 | 31 | |
| @@ -116,13 +122,26 @@ packages: | ||
| 116 | 122 | '@iden3/binfileutils@0.0.12': |
| 117 | 123 | resolution: {integrity: sha512-naAmzuDufRIcoNfQ1d99d7hGHufLA3wZSibtr4dMe6ZeiOPV1KwOZWTJ1YVz4HbaWlpDuzVU72dS4ATQS4PXBQ==} |
| 118 | 124 | |
| 125 | + '@noble/curves@1.2.0': | |
| 126 | + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} | |
| 127 | + | |
| 128 | + '@noble/hashes@1.3.2': | |
| 129 | + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} | |
| 130 | + engines: {node: '>= 16'} | |
| 131 | + | |
| 119 | 132 | '@noble/hashes@1.8.0': |
| 120 | 133 | resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} |
| 121 | 134 | engines: {node: ^14.21.3 || >=16} |
| 122 | 135 | |
| 136 | + '@types/node@22.7.5': | |
| 137 | + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} | |
| 138 | + | |
| 123 | 139 | aes-js@3.0.0: |
| 124 | 140 | resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} |
| 125 | 141 | |
| 142 | + aes-js@4.0.0-beta.5: | |
| 143 | + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} | |
| 144 | + | |
| 126 | 145 | async@3.2.6: |
| 127 | 146 | resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} |
| 128 | 147 | |
| @@ -216,6 +235,10 @@ packages: | ||
| 216 | 235 | ethers@5.8.0: |
| 217 | 236 | resolution: {integrity: sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==} |
| 218 | 237 | |
| 238 | + ethers@6.17.0: | |
| 239 | + resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==} | |
| 240 | + engines: {node: '>=14.0.0'} | |
| 241 | + | |
| 219 | 242 | fastfile@0.0.20: |
| 220 | 243 | resolution: {integrity: sha512-r5ZDbgImvVWCP0lA/cGNgQcZqR+aYdFx3u+CtJqUE510pBUVGMn4ulL/iRTI4tACTYsNJ736uzFxEBXesPAktA==} |
| 221 | 244 | |
| @@ -311,9 +334,15 @@ packages: | ||
| 311 | 334 | tryer@1.0.1: |
| 312 | 335 | resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} |
| 313 | 336 | |
| 337 | + tslib@2.7.0: | |
| 338 | + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} | |
| 339 | + | |
| 314 | 340 | underscore@1.13.6: |
| 315 | 341 | resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} |
| 316 | 342 | |
| 343 | + undici-types@6.19.8: | |
| 344 | + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} | |
| 345 | + | |
| 317 | 346 | util-deprecate@1.0.2: |
| 318 | 347 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} |
| 319 | 348 | |
| @@ -338,8 +367,22 @@ packages: | ||
| 338 | 367 | utf-8-validate: |
| 339 | 368 | optional: true |
| 340 | 369 | |
| 370 | + ws@8.21.0: | |
| 371 | + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} | |
| 372 | + engines: {node: '>=10.0.0'} | |
| 373 | + peerDependencies: | |
| 374 | + bufferutil: ^4.0.1 | |
| 375 | + utf-8-validate: '>=5.0.2' | |
| 376 | + peerDependenciesMeta: | |
| 377 | + bufferutil: | |
| 378 | + optional: true | |
| 379 | + utf-8-validate: | |
| 380 | + optional: true | |
| 381 | + | |
| 341 | 382 | snapshots: |
| 342 | 383 | |
| 384 | + '@adraffy/ens-normalize@1.11.1': {} | |
| 385 | + | |
| 343 | 386 | '@ethersproject/abi@5.8.0': |
| 344 | 387 | dependencies: |
| 345 | 388 | '@ethersproject/address': 5.8.0 |
| @@ -602,10 +645,22 @@ snapshots: | ||
| 602 | 645 | fastfile: 0.0.20 |
| 603 | 646 | ffjavascript: 0.3.1 |
| 604 | 647 | |
| 648 | + '@noble/curves@1.2.0': | |
| 649 | + dependencies: | |
| 650 | + '@noble/hashes': 1.3.2 | |
| 651 | + | |
| 652 | + '@noble/hashes@1.3.2': {} | |
| 653 | + | |
| 605 | 654 | '@noble/hashes@1.8.0': {} |
| 606 | 655 | |
| 656 | + '@types/node@22.7.5': | |
| 657 | + dependencies: | |
| 658 | + undici-types: 6.19.8 | |
| 659 | + | |
| 607 | 660 | aes-js@3.0.0: {} |
| 608 | 661 | |
| 662 | + aes-js@4.0.0-beta.5: {} | |
| 663 | + | |
| 609 | 664 | async@3.2.6: {} |
| 610 | 665 | |
| 611 | 666 | b4a@1.9.0: {} |
| @@ -739,6 +794,19 @@ snapshots: | ||
| 739 | 794 | - bufferutil |
| 740 | 795 | - utf-8-validate |
| 741 | 796 | |
| 797 | + ethers@6.17.0: | |
| 798 | + dependencies: | |
| 799 | + '@adraffy/ens-normalize': 1.11.1 | |
| 800 | + '@noble/curves': 1.2.0 | |
| 801 | + '@noble/hashes': 1.3.2 | |
| 802 | + '@types/node': 22.7.5 | |
| 803 | + aes-js: 4.0.0-beta.5 | |
| 804 | + tslib: 2.7.0 | |
| 805 | + ws: 8.21.0 | |
| 806 | + transitivePeerDependencies: | |
| 807 | + - bufferutil | |
| 808 | + - utf-8-validate | |
| 809 | + | |
| 742 | 810 | fastfile@0.0.20: {} |
| 743 | 811 | |
| 744 | 812 | ffjavascript@0.2.63: |
| @@ -852,8 +920,12 @@ snapshots: | ||
| 852 | 920 | |
| 853 | 921 | tryer@1.0.1: {} |
| 854 | 922 | |
| 923 | + tslib@2.7.0: {} | |
| 924 | + | |
| 855 | 925 | underscore@1.13.6: {} |
| 856 | 926 | |
| 927 | + undici-types@6.19.8: {} | |
| 928 | + | |
| 857 | 929 | util-deprecate@1.0.2: {} |
| 858 | 930 | |
| 859 | 931 | wasmbuilder@0.0.16: {} |
| @@ -865,3 +937,5 @@ snapshots: | ||
| 865 | 937 | web-worker@1.2.0: {} |
| 866 | 938 | |
| 867 | 939 | ws@8.18.0: {} |
| 940 | + | |
| 941 | + ws@8.21.0: {} | |
| @@ -14,12 +14,18 @@ importers: | |||
| 14 | circomlibjs: | 14 | circomlibjs: |
| 15 | specifier: ^0.1.7 | 15 | specifier: ^0.1.7 |
| 16 | version: 0.1.7 | 16 | version: 0.1.7 |
| 17 | + ethers: | ||
| 18 | + specifier: ^6.17.0 | ||
| 19 | + version: 6.17.0 | ||
| 17 | snarkjs: | 20 | snarkjs: |
| 18 | specifier: ^0.7.6 | 21 | specifier: ^0.7.6 |
| 19 | version: 0.7.6 | 22 | version: 0.7.6 |
| 20 | 23 | ||
| 21 | packages: | 24 | packages: |
| 22 | 25 | ||
| 26 | + '@adraffy/ens-normalize@1.11.1': | ||
| 27 | + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} | ||
| 28 | + | ||
| 23 | '@ethersproject/abi@5.8.0': | 29 | '@ethersproject/abi@5.8.0': |
| 24 | resolution: {integrity: sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==} | 30 | resolution: {integrity: sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==} |
| 25 | 31 | ||
| @@ -116,13 +122,26 @@ packages: | |||
| 116 | '@iden3/binfileutils@0.0.12': | 122 | '@iden3/binfileutils@0.0.12': |
| 117 | resolution: {integrity: sha512-naAmzuDufRIcoNfQ1d99d7hGHufLA3wZSibtr4dMe6ZeiOPV1KwOZWTJ1YVz4HbaWlpDuzVU72dS4ATQS4PXBQ==} | 123 | resolution: {integrity: sha512-naAmzuDufRIcoNfQ1d99d7hGHufLA3wZSibtr4dMe6ZeiOPV1KwOZWTJ1YVz4HbaWlpDuzVU72dS4ATQS4PXBQ==} |
| 118 | 124 | ||
| 125 | + '@noble/curves@1.2.0': | ||
| 126 | + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} | ||
| 127 | + | ||
| 128 | + '@noble/hashes@1.3.2': | ||
| 129 | + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} | ||
| 130 | + engines: {node: '>= 16'} | ||
| 131 | + | ||
| 119 | '@noble/hashes@1.8.0': | 132 | '@noble/hashes@1.8.0': |
| 120 | resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} | 133 | resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} |
| 121 | engines: {node: ^14.21.3 || >=16} | 134 | engines: {node: ^14.21.3 || >=16} |
| 122 | 135 | ||
| 136 | + '@types/node@22.7.5': | ||
| 137 | + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} | ||
| 138 | + | ||
| 123 | aes-js@3.0.0: | 139 | aes-js@3.0.0: |
| 124 | resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} | 140 | resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} |
| 125 | 141 | ||
| 142 | + aes-js@4.0.0-beta.5: | ||
| 143 | + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} | ||
| 144 | + | ||
| 126 | async@3.2.6: | 145 | async@3.2.6: |
| 127 | resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} | 146 | resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} |
| 128 | 147 | ||
| @@ -216,6 +235,10 @@ packages: | |||
| 216 | ethers@5.8.0: | 235 | ethers@5.8.0: |
| 217 | resolution: {integrity: sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==} | 236 | resolution: {integrity: sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==} |
| 218 | 237 | ||
| 238 | + ethers@6.17.0: | ||
| 239 | + resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==} | ||
| 240 | + engines: {node: '>=14.0.0'} | ||
| 241 | + | ||
| 219 | fastfile@0.0.20: | 242 | fastfile@0.0.20: |
| 220 | resolution: {integrity: sha512-r5ZDbgImvVWCP0lA/cGNgQcZqR+aYdFx3u+CtJqUE510pBUVGMn4ulL/iRTI4tACTYsNJ736uzFxEBXesPAktA==} | 243 | resolution: {integrity: sha512-r5ZDbgImvVWCP0lA/cGNgQcZqR+aYdFx3u+CtJqUE510pBUVGMn4ulL/iRTI4tACTYsNJ736uzFxEBXesPAktA==} |
| 221 | 244 | ||
| @@ -311,9 +334,15 @@ packages: | |||
| 311 | tryer@1.0.1: | 334 | tryer@1.0.1: |
| 312 | resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} | 335 | resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} |
| 313 | 336 | ||
| 337 | + tslib@2.7.0: | ||
| 338 | + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} | ||
| 339 | + | ||
| 314 | underscore@1.13.6: | 340 | underscore@1.13.6: |
| 315 | resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} | 341 | resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} |
| 316 | 342 | ||
| 343 | + undici-types@6.19.8: | ||
| 344 | + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} | ||
| 345 | + | ||
| 317 | util-deprecate@1.0.2: | 346 | util-deprecate@1.0.2: |
| 318 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} | 347 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} |
| 319 | 348 | ||
| @@ -338,8 +367,22 @@ packages: | |||
| 338 | utf-8-validate: | 367 | utf-8-validate: |
| 339 | optional: true | 368 | optional: true |
| 340 | 369 | ||
| 370 | + ws@8.21.0: | ||
| 371 | + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} | ||
| 372 | + engines: {node: '>=10.0.0'} | ||
| 373 | + peerDependencies: | ||
| 374 | + bufferutil: ^4.0.1 | ||
| 375 | + utf-8-validate: '>=5.0.2' | ||
| 376 | + peerDependenciesMeta: | ||
| 377 | + bufferutil: | ||
| 378 | + optional: true | ||
| 379 | + utf-8-validate: | ||
| 380 | + optional: true | ||
| 381 | + | ||
| 341 | snapshots: | 382 | snapshots: |
| 342 | 383 | ||
| 384 | + '@adraffy/ens-normalize@1.11.1': {} | ||
| 385 | + | ||
| 343 | '@ethersproject/abi@5.8.0': | 386 | '@ethersproject/abi@5.8.0': |
| 344 | dependencies: | 387 | dependencies: |
| 345 | '@ethersproject/address': 5.8.0 | 388 | '@ethersproject/address': 5.8.0 |
| @@ -602,10 +645,22 @@ snapshots: | |||
| 602 | fastfile: 0.0.20 | 645 | fastfile: 0.0.20 |
| 603 | ffjavascript: 0.3.1 | 646 | ffjavascript: 0.3.1 |
| 604 | 647 | ||
| 648 | + '@noble/curves@1.2.0': | ||
| 649 | + dependencies: | ||
| 650 | + '@noble/hashes': 1.3.2 | ||
| 651 | + | ||
| 652 | + '@noble/hashes@1.3.2': {} | ||
| 653 | + | ||
| 605 | '@noble/hashes@1.8.0': {} | 654 | '@noble/hashes@1.8.0': {} |
| 606 | 655 | ||
| 656 | + '@types/node@22.7.5': | ||
| 657 | + dependencies: | ||
| 658 | + undici-types: 6.19.8 | ||
| 659 | + | ||
| 607 | aes-js@3.0.0: {} | 660 | aes-js@3.0.0: {} |
| 608 | 661 | ||
| 662 | + aes-js@4.0.0-beta.5: {} | ||
| 663 | + | ||
| 609 | async@3.2.6: {} | 664 | async@3.2.6: {} |
| 610 | 665 | ||
| 611 | b4a@1.9.0: {} | 666 | b4a@1.9.0: {} |
| @@ -739,6 +794,19 @@ snapshots: | |||
| 739 | - bufferutil | 794 | - bufferutil |
| 740 | - utf-8-validate | 795 | - utf-8-validate |
| 741 | 796 | ||
| 797 | + ethers@6.17.0: | ||
| 798 | + dependencies: | ||
| 799 | + '@adraffy/ens-normalize': 1.11.1 | ||
| 800 | + '@noble/curves': 1.2.0 | ||
| 801 | + '@noble/hashes': 1.3.2 | ||
| 802 | + '@types/node': 22.7.5 | ||
| 803 | + aes-js: 4.0.0-beta.5 | ||
| 804 | + tslib: 2.7.0 | ||
| 805 | + ws: 8.21.0 | ||
| 806 | + transitivePeerDependencies: | ||
| 807 | + - bufferutil | ||
| 808 | + - utf-8-validate | ||
| 809 | + | ||
| 742 | fastfile@0.0.20: {} | 810 | fastfile@0.0.20: {} |
| 743 | 811 | ||
| 744 | ffjavascript@0.2.63: | 812 | ffjavascript@0.2.63: |
| @@ -852,8 +920,12 @@ snapshots: | |||
| 852 | 920 | ||
| 853 | tryer@1.0.1: {} | 921 | tryer@1.0.1: {} |
| 854 | 922 | ||
| 923 | + tslib@2.7.0: {} | ||
| 924 | + | ||
| 855 | underscore@1.13.6: {} | 925 | underscore@1.13.6: {} |
| 856 | 926 | ||
| 927 | + undici-types@6.19.8: {} | ||
| 928 | + | ||
| 857 | util-deprecate@1.0.2: {} | 929 | util-deprecate@1.0.2: {} |
| 858 | 930 | ||
| 859 | wasmbuilder@0.0.16: {} | 931 | wasmbuilder@0.0.16: {} |
| @@ -865,3 +937,5 @@ snapshots: | |||
| 865 | web-worker@1.2.0: {} | 937 | web-worker@1.2.0: {} |
| 866 | 938 | ||
| 867 | ws@8.18.0: {} | 939 | ws@8.18.0: {} |
| 940 | + | ||
| 941 | + ws@8.21.0: {} | ||
added
zk/scripts/e2e.mjs +162 -0 | new file mode 100644 | ||
| @@ -0,0 +1,162 @@ | ||
| 1 | +// End-to-end run of the private pool against a local anvil: deploy with the real | |
| 2 | +// script, deposit three notes, prove with zk/lib.mjs, claim, and try to break it. | |
| 3 | +// | |
| 4 | +// This is what docs/ZK.md tells a user to do, so it is written the same way. | |
| 5 | +// Needs `anvil` and `forge` on PATH. Run it with `pnpm e2e`. | |
| 6 | +import assert from "node:assert/strict"; | |
| 7 | +import { execFileSync, spawn } from "node:child_process"; | |
| 8 | +import { mkdtempSync, readFileSync } from "node:fs"; | |
| 9 | +import { tmpdir } from "node:os"; | |
| 10 | +import { join } from "node:path"; | |
| 11 | +import { fileURLToPath } from "node:url"; | |
| 12 | +import { Contract, HDNodeWallet, JsonRpcProvider, Network, Wallet } from "ethers"; | |
| 13 | +import { buildTree, decodeNote, encodeNote, newNote, noteHashes, prove } from "../lib.mjs"; | |
| 14 | + | |
| 15 | +const ROOT = fileURLToPath(new URL("../../", import.meta.url)); | |
| 16 | +const PORT = process.env.E2E_PORT ?? "8599"; | |
| 17 | +const RPC = `http://127.0.0.1:${PORT}`; | |
| 18 | + | |
| 19 | +// The registry only accepts wETH code it has reviewed for the chain it is on, so | |
| 20 | +// the chain is Base and the wETH is Base's, put at the OP Stack predeploy address. | |
| 21 | +const CHAIN_ID = 8453; | |
| 22 | +const WETH_AT = "0x4200000000000000000000000000000000000006"; | |
| 23 | +const WETH_CODE = readFileSync(join(ROOT, "test/fixtures/weth9-op-legacy.hex"), "utf8").trim(); | |
| 24 | + | |
| 25 | +const MNEMONIC = "test test test test test test test test test test test junk"; | |
| 26 | +const account = (i) => HDNodeWallet.fromPhrase(MNEMONIC, "", `m/44'/60'/0'/0/${i}`); | |
| 27 | + | |
| 28 | +const WETH_ABI = [ | |
| 29 | + "function deposit() payable", | |
| 30 | + "function approve(address spender, uint256 amount) returns (bool)", | |
| 31 | +]; | |
| 32 | +const LOVE_ABI = [ | |
| 33 | + "function DENOMINATION() view returns (uint256)", | |
| 34 | + "function RATE() view returns (uint256)", | |
| 35 | + "function balanceOf(address account) view returns (uint256)", | |
| 36 | + "function nextLeafIndex() view returns (uint256)", | |
| 37 | + "function isKnownRoot(bytes32 root) view returns (bool)", | |
| 38 | + "function isSpent(bytes32 nullifierHash) view returns (bool)", | |
| 39 | + "function deposit(uint256 wethAmount)", | |
| 40 | + "function deposit(bytes32 commitment)", | |
| 41 | + "function claim(uint256[2] pA, uint256[2][2] pB, uint256[2] pC, bytes32 root, bytes32 nullifierHash, address recipient)", | |
| 42 | + "event Deposit(address indexed account, uint256 wethAmount, uint256 loveAmount)", | |
| 43 | + "event Deposit(bytes32 indexed commitment, uint256 leafIndex)", | |
| 44 | + "event Claim(address indexed recipient, bytes32 nullifierHash)", | |
| 45 | + "error NoteAlreadySpent(bytes32 nullifierHash)", | |
| 46 | + "error InvalidProof()", | |
| 47 | +]; | |
| 48 | + | |
| 49 | +const step = (msg) => console.log(`- ${msg}`); | |
| 50 | +const revertsWith = (name) => (e) => e.revert?.name === name; | |
| 51 | + | |
| 52 | +async function startAnvil() { | |
| 53 | + const anvil = spawn("anvil", ["--chain-id", String(CHAIN_ID), "--port", PORT, "--silent"], { stdio: "inherit" }); | |
| 54 | + anvil.on("error", (e) => { | |
| 55 | + console.error(`could not start anvil: ${e.message}`); | |
| 56 | + process.exit(1); | |
| 57 | + }); | |
| 58 | + | |
| 59 | + const provider = new JsonRpcProvider(RPC, undefined, { | |
| 60 | + staticNetwork: Network.from(CHAIN_ID), | |
| 61 | + // Back-to-back transactions from one wallet would otherwise reuse a cached nonce. | |
| 62 | + cacheTimeout: -1, | |
| 63 | + }); | |
| 64 | + for (let i = 0; i < 50; i++) { | |
| 65 | + try { | |
| 66 | + await provider.getBlockNumber(); | |
| 67 | + return { anvil, provider }; | |
| 68 | + } catch { | |
| 69 | + await new Promise((r) => setTimeout(r, 200)); | |
| 70 | + } | |
| 71 | + } | |
| 72 | + anvil.kill(); | |
| 73 | + throw new Error(`anvil did not come up on ${RPC}`); | |
| 74 | +} | |
| 75 | + | |
| 76 | +// Runs script/Love.s.sol as a user would, and reads the token's address off its log. | |
| 77 | +function deployLove(deployer) { | |
| 78 | + // Keeps the broadcast log out of the repo. | |
| 79 | + const broadcast = mkdtempSync(join(tmpdir(), "love-e2e-")); | |
| 80 | + const out = execFileSync( | |
| 81 | + "forge", | |
| 82 | + ["script", "script/Love.s.sol", "--rpc-url", RPC, "--broadcast", "--private-key", deployer.privateKey], | |
| 83 | + { cwd: ROOT, encoding: "utf8", env: { ...process.env, FOUNDRY_BROADCAST: broadcast } }, | |
| 84 | + ); | |
| 85 | + const address = out.match(/Love deployed at\s+(0x[0-9a-fA-F]{40})/)?.[1]; | |
| 86 | + assert.ok(address, `no "Love deployed at" line in the script output:\n${out}`); | |
| 87 | + return address; | |
| 88 | +} | |
| 89 | + | |
| 90 | +async function main() { | |
| 91 | + const { anvil, provider } = await startAnvil(); | |
| 92 | + try { | |
| 93 | + const [deployer, alice, bob, carol, claimer] = [0, 1, 2, 3, 4].map((i) => account(i).connect(provider)); | |
| 94 | + const recipient = Wallet.createRandom().address; | |
| 95 | + | |
| 96 | + step("deploying LOVE"); | |
| 97 | + await provider.send("anvil_setCode", [WETH_AT, WETH_CODE.startsWith("0x") ? WETH_CODE : `0x${WETH_CODE}`]); | |
| 98 | + const love = new Contract(deployLove(deployer), LOVE_ABI, provider); | |
| 99 | + const denomination = await love.DENOMINATION(); | |
| 100 | + const noteLove = denomination * (await love.RATE()); | |
| 101 | + | |
| 102 | + step("depositing three notes"); | |
| 103 | + const notes = []; | |
| 104 | + for (const depositor of [alice, bob, carol]) { | |
| 105 | + const note = newNote(); | |
| 106 | + const { commitment } = await noteHashes(note); | |
| 107 | + | |
| 108 | + const weth = new Contract(WETH_AT, WETH_ABI, depositor); | |
| 109 | + await (await weth.deposit({ value: denomination })).wait(); | |
| 110 | + await (await weth.approve(love.target, denomination)).wait(); | |
| 111 | + await (await love.connect(depositor).getFunction("deposit(bytes32)")(commitment)).wait(); | |
| 112 | + | |
| 113 | + notes.push(encodeNote(note)); | |
| 114 | + } | |
| 115 | + assert.equal(await love.nextLeafIndex(), 3n); | |
| 116 | + | |
| 117 | + step("rebuilding the tree from Deposit events"); | |
| 118 | + const logs = await love.queryFilter(love.filters["Deposit(bytes32,uint256)"]()); | |
| 119 | + const commitments = logs | |
| 120 | + .sort((a, b) => Number(a.args.leafIndex - b.args.leafIndex)) | |
| 121 | + .map((log) => log.args.commitment); | |
| 122 | + assert.equal(commitments.length, 3); | |
| 123 | + assert.ok(await love.isKnownRoot((await buildTree(commitments)).root), "off-chain root is unknown on-chain"); | |
| 124 | + | |
| 125 | + step("proving and claiming note 1 from a fresh address"); | |
| 126 | + const claim = await prove({ note: decodeNote(notes[1]), commitments, recipient }); | |
| 127 | + const args = [claim.pA, claim.pB, claim.pC, claim.root, claim.nullifierHash, claim.recipient]; | |
| 128 | + | |
| 129 | + await (await love.connect(claimer).claim(...args)).wait(); | |
| 130 | + assert.equal(await love.balanceOf(recipient), noteLove); | |
| 131 | + assert.ok(await love.isSpent(claim.nullifierHash)); | |
| 132 | + | |
| 133 | + step("claiming note 0 against the root from after its own deposit"); | |
| 134 | + const old = await prove({ note: decodeNote(notes[0]), commitments: commitments.slice(0, 1), recipient }); | |
| 135 | + await (await love.connect(claimer).claim(old.pA, old.pB, old.pC, old.root, old.nullifierHash, recipient)).wait(); | |
| 136 | + assert.equal(await love.balanceOf(recipient), 2n * noteLove); | |
| 137 | + | |
| 138 | + step("checking a spent note, and a copied claim, are refused"); | |
| 139 | + await assert.rejects(love.connect(claimer).claim.staticCall(...args), revertsWith("NoteAlreadySpent")); | |
| 140 | + const fresh = await prove({ note: decodeNote(notes[2]), commitments, recipient }); | |
| 141 | + await assert.rejects( | |
| 142 | + love | |
| 143 | + .connect(claimer) | |
| 144 | + .claim.staticCall(fresh.pA, fresh.pB, fresh.pC, fresh.root, fresh.nullifierHash, claimer.address), | |
| 145 | + revertsWith("InvalidProof"), | |
| 146 | + ); | |
| 147 | + assert.equal(await love.isSpent(fresh.nullifierHash), false); | |
| 148 | + | |
| 149 | + console.log("ok"); | |
| 150 | + } finally { | |
| 151 | + anvil.kill(); | |
| 152 | + } | |
| 153 | +} | |
| 154 | + | |
| 155 | +main().then( | |
| 156 | + // snarkjs keeps worker threads alive. | |
| 157 | + () => process.exit(0), | |
| 158 | + (e) => { | |
| 159 | + console.error(e); | |
| 160 | + process.exit(1); | |
| 161 | + }, | |
| 162 | +); | |
| new file mode 100644 | |||
| @@ -0,0 +1,162 @@ | |||
| 1 | +// End-to-end run of the private pool against a local anvil: deploy with the real | ||
| 2 | +// script, deposit three notes, prove with zk/lib.mjs, claim, and try to break it. | ||
| 3 | +// | ||
| 4 | +// This is what docs/ZK.md tells a user to do, so it is written the same way. | ||
| 5 | +// Needs `anvil` and `forge` on PATH. Run it with `pnpm e2e`. | ||
| 6 | +import assert from "node:assert/strict"; | ||
| 7 | +import { execFileSync, spawn } from "node:child_process"; | ||
| 8 | +import { mkdtempSync, readFileSync } from "node:fs"; | ||
| 9 | +import { tmpdir } from "node:os"; | ||
| 10 | +import { join } from "node:path"; | ||
| 11 | +import { fileURLToPath } from "node:url"; | ||
| 12 | +import { Contract, HDNodeWallet, JsonRpcProvider, Network, Wallet } from "ethers"; | ||
| 13 | +import { buildTree, decodeNote, encodeNote, newNote, noteHashes, prove } from "../lib.mjs"; | ||
| 14 | + | ||
| 15 | +const ROOT = fileURLToPath(new URL("../../", import.meta.url)); | ||
| 16 | +const PORT = process.env.E2E_PORT ?? "8599"; | ||
| 17 | +const RPC = `http://127.0.0.1:${PORT}`; | ||
| 18 | + | ||
| 19 | +// The registry only accepts wETH code it has reviewed for the chain it is on, so | ||
| 20 | +// the chain is Base and the wETH is Base's, put at the OP Stack predeploy address. | ||
| 21 | +const CHAIN_ID = 8453; | ||
| 22 | +const WETH_AT = "0x4200000000000000000000000000000000000006"; | ||
| 23 | +const WETH_CODE = readFileSync(join(ROOT, "test/fixtures/weth9-op-legacy.hex"), "utf8").trim(); | ||
| 24 | + | ||
| 25 | +const MNEMONIC = "test test test test test test test test test test test junk"; | ||
| 26 | +const account = (i) => HDNodeWallet.fromPhrase(MNEMONIC, "", `m/44'/60'/0'/0/${i}`); | ||
| 27 | + | ||
| 28 | +const WETH_ABI = [ | ||
| 29 | + "function deposit() payable", | ||
| 30 | + "function approve(address spender, uint256 amount) returns (bool)", | ||
| 31 | +]; | ||
| 32 | +const LOVE_ABI = [ | ||
| 33 | + "function DENOMINATION() view returns (uint256)", | ||
| 34 | + "function RATE() view returns (uint256)", | ||
| 35 | + "function balanceOf(address account) view returns (uint256)", | ||
| 36 | + "function nextLeafIndex() view returns (uint256)", | ||
| 37 | + "function isKnownRoot(bytes32 root) view returns (bool)", | ||
| 38 | + "function isSpent(bytes32 nullifierHash) view returns (bool)", | ||
| 39 | + "function deposit(uint256 wethAmount)", | ||
| 40 | + "function deposit(bytes32 commitment)", | ||
| 41 | + "function claim(uint256[2] pA, uint256[2][2] pB, uint256[2] pC, bytes32 root, bytes32 nullifierHash, address recipient)", | ||
| 42 | + "event Deposit(address indexed account, uint256 wethAmount, uint256 loveAmount)", | ||
| 43 | + "event Deposit(bytes32 indexed commitment, uint256 leafIndex)", | ||
| 44 | + "event Claim(address indexed recipient, bytes32 nullifierHash)", | ||
| 45 | + "error NoteAlreadySpent(bytes32 nullifierHash)", | ||
| 46 | + "error InvalidProof()", | ||
| 47 | +]; | ||
| 48 | + | ||
| 49 | +const step = (msg) => console.log(`- ${msg}`); | ||
| 50 | +const revertsWith = (name) => (e) => e.revert?.name === name; | ||
| 51 | + | ||
| 52 | +async function startAnvil() { | ||
| 53 | + const anvil = spawn("anvil", ["--chain-id", String(CHAIN_ID), "--port", PORT, "--silent"], { stdio: "inherit" }); | ||
| 54 | + anvil.on("error", (e) => { | ||
| 55 | + console.error(`could not start anvil: ${e.message}`); | ||
| 56 | + process.exit(1); | ||
| 57 | + }); | ||
| 58 | + | ||
| 59 | + const provider = new JsonRpcProvider(RPC, undefined, { | ||
| 60 | + staticNetwork: Network.from(CHAIN_ID), | ||
| 61 | + // Back-to-back transactions from one wallet would otherwise reuse a cached nonce. | ||
| 62 | + cacheTimeout: -1, | ||
| 63 | + }); | ||
| 64 | + for (let i = 0; i < 50; i++) { | ||
| 65 | + try { | ||
| 66 | + await provider.getBlockNumber(); | ||
| 67 | + return { anvil, provider }; | ||
| 68 | + } catch { | ||
| 69 | + await new Promise((r) => setTimeout(r, 200)); | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + anvil.kill(); | ||
| 73 | + throw new Error(`anvil did not come up on ${RPC}`); | ||
| 74 | +} | ||
| 75 | + | ||
| 76 | +// Runs script/Love.s.sol as a user would, and reads the token's address off its log. | ||
| 77 | +function deployLove(deployer) { | ||
| 78 | + // Keeps the broadcast log out of the repo. | ||
| 79 | + const broadcast = mkdtempSync(join(tmpdir(), "love-e2e-")); | ||
| 80 | + const out = execFileSync( | ||
| 81 | + "forge", | ||
| 82 | + ["script", "script/Love.s.sol", "--rpc-url", RPC, "--broadcast", "--private-key", deployer.privateKey], | ||
| 83 | + { cwd: ROOT, encoding: "utf8", env: { ...process.env, FOUNDRY_BROADCAST: broadcast } }, | ||
| 84 | + ); | ||
| 85 | + const address = out.match(/Love deployed at\s+(0x[0-9a-fA-F]{40})/)?.[1]; | ||
| 86 | + assert.ok(address, `no "Love deployed at" line in the script output:\n${out}`); | ||
| 87 | + return address; | ||
| 88 | +} | ||
| 89 | + | ||
| 90 | +async function main() { | ||
| 91 | + const { anvil, provider } = await startAnvil(); | ||
| 92 | + try { | ||
| 93 | + const [deployer, alice, bob, carol, claimer] = [0, 1, 2, 3, 4].map((i) => account(i).connect(provider)); | ||
| 94 | + const recipient = Wallet.createRandom().address; | ||
| 95 | + | ||
| 96 | + step("deploying LOVE"); | ||
| 97 | + await provider.send("anvil_setCode", [WETH_AT, WETH_CODE.startsWith("0x") ? WETH_CODE : `0x${WETH_CODE}`]); | ||
| 98 | + const love = new Contract(deployLove(deployer), LOVE_ABI, provider); | ||
| 99 | + const denomination = await love.DENOMINATION(); | ||
| 100 | + const noteLove = denomination * (await love.RATE()); | ||
| 101 | + | ||
| 102 | + step("depositing three notes"); | ||
| 103 | + const notes = []; | ||
| 104 | + for (const depositor of [alice, bob, carol]) { | ||
| 105 | + const note = newNote(); | ||
| 106 | + const { commitment } = await noteHashes(note); | ||
| 107 | + | ||
| 108 | + const weth = new Contract(WETH_AT, WETH_ABI, depositor); | ||
| 109 | + await (await weth.deposit({ value: denomination })).wait(); | ||
| 110 | + await (await weth.approve(love.target, denomination)).wait(); | ||
| 111 | + await (await love.connect(depositor).getFunction("deposit(bytes32)")(commitment)).wait(); | ||
| 112 | + | ||
| 113 | + notes.push(encodeNote(note)); | ||
| 114 | + } | ||
| 115 | + assert.equal(await love.nextLeafIndex(), 3n); | ||
| 116 | + | ||
| 117 | + step("rebuilding the tree from Deposit events"); | ||
| 118 | + const logs = await love.queryFilter(love.filters["Deposit(bytes32,uint256)"]()); | ||
| 119 | + const commitments = logs | ||
| 120 | + .sort((a, b) => Number(a.args.leafIndex - b.args.leafIndex)) | ||
| 121 | + .map((log) => log.args.commitment); | ||
| 122 | + assert.equal(commitments.length, 3); | ||
| 123 | + assert.ok(await love.isKnownRoot((await buildTree(commitments)).root), "off-chain root is unknown on-chain"); | ||
| 124 | + | ||
| 125 | + step("proving and claiming note 1 from a fresh address"); | ||
| 126 | + const claim = await prove({ note: decodeNote(notes[1]), commitments, recipient }); | ||
| 127 | + const args = [claim.pA, claim.pB, claim.pC, claim.root, claim.nullifierHash, claim.recipient]; | ||
| 128 | + | ||
| 129 | + await (await love.connect(claimer).claim(...args)).wait(); | ||
| 130 | + assert.equal(await love.balanceOf(recipient), noteLove); | ||
| 131 | + assert.ok(await love.isSpent(claim.nullifierHash)); | ||
| 132 | + | ||
| 133 | + step("claiming note 0 against the root from after its own deposit"); | ||
| 134 | + const old = await prove({ note: decodeNote(notes[0]), commitments: commitments.slice(0, 1), recipient }); | ||
| 135 | + await (await love.connect(claimer).claim(old.pA, old.pB, old.pC, old.root, old.nullifierHash, recipient)).wait(); | ||
| 136 | + assert.equal(await love.balanceOf(recipient), 2n * noteLove); | ||
| 137 | + | ||
| 138 | + step("checking a spent note, and a copied claim, are refused"); | ||
| 139 | + await assert.rejects(love.connect(claimer).claim.staticCall(...args), revertsWith("NoteAlreadySpent")); | ||
| 140 | + const fresh = await prove({ note: decodeNote(notes[2]), commitments, recipient }); | ||
| 141 | + await assert.rejects( | ||
| 142 | + love | ||
| 143 | + .connect(claimer) | ||
| 144 | + .claim.staticCall(fresh.pA, fresh.pB, fresh.pC, fresh.root, fresh.nullifierHash, claimer.address), | ||
| 145 | + revertsWith("InvalidProof"), | ||
| 146 | + ); | ||
| 147 | + assert.equal(await love.isSpent(fresh.nullifierHash), false); | ||
| 148 | + | ||
| 149 | + console.log("ok"); | ||
| 150 | + } finally { | ||
| 151 | + anvil.kill(); | ||
| 152 | + } | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +main().then( | ||
| 156 | + // snarkjs keeps worker threads alive. | ||
| 157 | + () => process.exit(0), | ||
| 158 | + (e) => { | ||
| 159 | + console.error(e); | ||
| 160 | + process.exit(1); | ||
| 161 | + }, | ||
| 162 | +); | ||