commit d464eef3f6f6940b6951e657248db1d552db1d8a Author: poma Date: Mon Feb 1 16:40:32 2021 +0300 init diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..301ea61 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.git +Dockerfile +cache +artifacts \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..911d315 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,27 @@ +{ + "env": { + "node": true, + "browser": true, + "es6": true, + "mocha": true + }, + "extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier"], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "indent": ["error", 2], + "linebreak-style": ["error", "unix"], + "quotes": ["error", "single"], + "semi": ["error", "never"], + "object-curly-spacing": ["error", "always"], + "comma-dangle": ["error", "always-multiline"], + "require-await": "error", + "prettier/prettier": ["error", { "printWidth": 110 }] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..52031de --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sol linguist-language=Solidity diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..784148b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules + +#Hardhat files +cache +artifacts diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +12 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b4a1091 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +.vscode +build +circuits +scripts +contracts/verifiers/RewardVerifier.sol +contracts/verifiers/WithdrawVerifier.sol +contracts/verifiers/TreeUpdateVerifier.sol +contracts/utils/FloatMath.sol diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f532b1b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "semi": false, + "printWidth": 110, + "overrides": [ + { + "files": "*.sol", + "options": { + "singleQuote": false, + "printWidth": 130 + } + } + ] +} diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..b313abe --- /dev/null +++ b/.solhint.json @@ -0,0 +1,13 @@ +{ + "extends": "solhint:recommended", + "rules": { + "prettier/prettier": [ + "error", + { + "printWidth": 110 + } + ], + "quotes": ["error", "double"] + }, + "plugins": ["prettier"] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..80a9888 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:14-buster + +ENTRYPOINT bash +RUN apt-get update && \ + apt-get install -y libgmp-dev nlohmann-json3-dev nasm g++ git curl && \ + rm -rf /var/lib/apt/lists/* + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" +RUN cargo install zkutil + +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn +COPY circuits circuits +COPY scripts scripts +# ENV NODE_OPTIONS='--trace-gc --trace-gc-ignore-scavenger --max-old-space-size=2048000 --initial-old-space-size=2048000 --no-global-gc-scheduling --no-incremental-marking --max-semi-space-size=1024 --initial-heap-size=2048000' +ENV NODE_OPTIONS='--max-old-space-size=2048000' +RUN mkdir -p build/circuits +RUN yarn circuit:batchTreeUpdateLarge +RUN yarn circuit:batchTreeUpdateWitness +COPY . . diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb98c92 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Truffle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..bbc09ca --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Tornado.cash trees [![Build Status](https://github.com/tornadocash/tornado-anonymity-mining/workflows/build/badge.svg)](https://github.com/tornadocash/tornado-anonymity-mining/actions) [![npm](https://img.shields.io/npm/v/tornado-anonymity-mining)](https://www.npmjs.com/package/tornado-anonymity-mining) + +This repo implements a more optimized version of the [TornadoTrees](https://github.com/tornadocash/tornado-anonymity-mining/blob/080d0f83665fa686d7fe42dd57fb5975d0f1ca58/contracts/TornadoTrees.sol) mechanism. + +## Dependencies + +1. node 12 +2. yarn +3. zkutil (`brew install rust && cargo install zkutil`) + +## Start + +```bash +$ yarn +$ cp .env.example .env +$ yarn circuit +$ yarn test +``` diff --git a/circuits/BatchTreeUpdate.circom b/circuits/BatchTreeUpdate.circom new file mode 100644 index 0000000..1bf0539 --- /dev/null +++ b/circuits/BatchTreeUpdate.circom @@ -0,0 +1,106 @@ +include "../node_modules/circomlib/circuits/poseidon.circom"; +include "../node_modules/circomlib/circuits/bitify.circom"; +include "./MerkleTreeUpdater.circom"; +include "./Utils.circom"; + +template TreeLayer(height) { + signal input ins[1 << (height + 1)]; + signal output outs[1 << height]; + + component hash[1 << height]; + for(var i = 0; i < (1 << height); i++) { + hash[i] = HashLeftRight(); + hash[i].left <== ins[i * 2]; + hash[i].right <== ins[i * 2 + 1]; + hash[i].hash ==> outs[i]; + } +} + +// Inserts a leaf batch into a tree +// Checks that tree previously contained zero leaves in the same position +template BatchTreeUpdate(levels, batchLevels, zeroBatchLeaf) { + var height = levels - batchLevels; + var nLeaves = 1 << batchLevels; + signal input argsHash; + signal private input oldRoot; + signal private input newRoot; + signal private input pathIndices; + signal private input pathElements[height]; + signal private input hashes[nLeaves]; + signal private input instances[nLeaves]; + signal private input blocks[nLeaves]; + + // Check that hash of arguments is correct + // We compress arguments into a single hash to considerably reduce gas usage on chain + component argsHasher = TreeUpdateArgsHasher(nLeaves); + argsHasher.oldRoot <== oldRoot; + argsHasher.newRoot <== newRoot; + argsHasher.pathIndices <== pathIndices; + for(var i = 0; i < nLeaves; i++) { + argsHasher.hashes[i] <== hashes[i]; + argsHasher.instances[i] <== instances[i]; + argsHasher.blocks[i] <== blocks[i]; + } + argsHash === argsHasher.out; + + // Compute hashes of all leaves + component leaves[nLeaves]; + for(var i = 0; i < nLeaves; i++) { + leaves[i] = Poseidon(3); + leaves[i].inputs[0] <== instances[i]; + leaves[i].inputs[1] <== hashes[i]; + leaves[i].inputs[2] <== blocks[i]; + } + + // Compute batch subtree merkle root + component layers[batchLevels]; + for(var level = batchLevels - 1; level >= 0; level--) { + layers[level] = TreeLayer(level); + for(var i = 0; i < (1 << (level + 1)); i++) { + layers[level].ins[i] <== level == batchLevels - 1 ? leaves[i].out : layers[level + 1].outs[i]; + } + } + + // Verify that batch subtree was inserted correctly + component treeUpdater = MerkleTreeUpdater(height, zeroBatchLeaf); + treeUpdater.oldRoot <== oldRoot; + treeUpdater.newRoot <== newRoot; + treeUpdater.leaf <== layers[0].outs[0]; + treeUpdater.pathIndices <== pathIndices; + for(var i = 0; i < height; i++) { + treeUpdater.pathElements[i] <== pathElements[i]; + } +} + +// zeroLeaf = keccak256("tornado") % FIELD_SIZE +// zeroBatchLeaf is poseidon(zeroLeaf, zeroLeaf) (batchLevels - 1) times +component main = BatchTreeUpdate(20, 2, 18183130938628345667957803100405002905363080101794697711581833408293369315484) + +// for mainnet use 20, 7, 17278668323652664881420209773995988768195998574629614593395162463145689805534 + +/* +circom has new incompatible poseidon. Temp zeroes list for it: //todo change before going to prod +21663839004416932945382355908790599225266501822907911457504978515578255421292 +6558759280185035534262768457043627242000481389071721366452285175835842504378 +18183130938628345667957803100405002905363080101794697711581833408293369315484 +1649420513912702105317807420227791840680436863908773772957989215330544908558 +1519470092809891639075459401377052367516123764590098800258495876383484284269 +11483247544333149638457900649034027061705091890934198553446561731884583952449 +1676192069326199968794155063658499005818740739752078691901135837045539426569 +3691667188955435348598133121052392834370851623525570836101013075316308823822 +1023697156307946028208019788980135577833715295171390138750488664899512032833 +*/ + +/* +zeros of n-th order: +21663839004416932945382355908790599225266501822907911457504978515578255421292 +11850551329423159860688778991827824730037759162201783566284850822760196767874 +21572503925325825116380792768937986743990254033176521064707045559165336555197 +11224495635916644180335675565949106569141882748352237685396337327907709534945 + 2399242030534463392142674970266584742013168677609861039634639961298697064915 +13182067204896548373877843501261957052850428877096289097123906067079378150834 + 7106632500398372645836762576259242192202230138343760620842346283595225511823 +17278668323652664881420209773995988768195998574629614593395162463145689805534 + 209436188287252095316293336871467217491997565239632454977424802439169726471 + 6509061943359659796226067852175931816441223836265895622135845733346450111408 +*/ diff --git a/circuits/MerkleTree.circom b/circuits/MerkleTree.circom new file mode 100644 index 0000000..c508e18 --- /dev/null +++ b/circuits/MerkleTree.circom @@ -0,0 +1,71 @@ +include "../node_modules/circomlib/circuits/poseidon.circom"; +include "../node_modules/circomlib/circuits/bitify.circom"; + +// Computes Poseidon([left, right]) +template HashLeftRight() { + signal input left; + signal input right; + signal output hash; + + component hasher = Poseidon(2); + hasher.inputs[0] <== left; + hasher.inputs[1] <== right; + hash <== hasher.out; +} + +// if s == 0 returns [in[0], in[1]] +// if s == 1 returns [in[1], in[0]] +template DualMux() { + signal input in[2]; + signal input s; + signal output out[2]; + + s * (1 - s) === 0; + out[0] <== (in[1] - in[0])*s + in[0]; + out[1] <== (in[0] - in[1])*s + in[1]; +} + +// Verifies that merkle proof is correct for given merkle root and a leaf +// pathIndices input is an array of 0/1 selectors telling whether given pathElement is on the left or right side of merkle path +template RawMerkleTree(levels) { + signal input leaf; + signal input pathElements[levels]; + signal input pathIndices[levels]; + + signal output root; + + component selectors[levels]; + component hashers[levels]; + + for (var i = 0; i < levels; i++) { + selectors[i] = DualMux(); + selectors[i].in[0] <== i == 0 ? leaf : hashers[i - 1].hash; + selectors[i].in[1] <== pathElements[i]; + selectors[i].s <== pathIndices[i]; + + hashers[i] = HashLeftRight(); + hashers[i].left <== selectors[i].out[0]; + hashers[i].right <== selectors[i].out[1]; + } + + root <== hashers[levels - 1].hash; +} + +template MerkleTree(levels) { + signal input leaf; + signal input pathElements[levels]; + signal input pathIndices; + signal output root; + + component indexBits = Num2Bits(levels); + indexBits.in <== pathIndices; + + component tree = RawMerkleTree(levels) + tree.leaf <== leaf; + for (var i = 0; i < levels; i++) { + tree.pathIndices[i] <== indexBits.out[i]; + tree.pathElements[i] <== pathElements[i]; + } + + root <== tree.root +} diff --git a/circuits/MerkleTreeUpdater.circom b/circuits/MerkleTreeUpdater.circom new file mode 100644 index 0000000..eb8a83b --- /dev/null +++ b/circuits/MerkleTreeUpdater.circom @@ -0,0 +1,33 @@ +include "./MerkleTree.circom"; + +// inserts a leaf into a tree +// checks that tree previously contained zero in the same position +template MerkleTreeUpdater(levels, zeroLeaf) { + signal input oldRoot; + signal input newRoot; + signal input leaf; + signal input pathIndices; + signal private input pathElements[levels]; + + // Compute indexBits once for both trees + // Since Num2Bits is non deterministic, 2 duplicate calls to it cannot be + // optimized by circom compiler + component indexBits = Num2Bits(levels); + indexBits.in <== pathIndices; + + component treeBefore = RawMerkleTree(levels); + for(var i = 0; i < levels; i++) { + treeBefore.pathIndices[i] <== indexBits.out[i]; + treeBefore.pathElements[i] <== pathElements[i]; + } + treeBefore.leaf <== zeroLeaf; + treeBefore.root === oldRoot; + + component treeAfter = RawMerkleTree(levels); + for(var i = 0; i < levels; i++) { + treeAfter.pathIndices[i] <== indexBits.out[i]; + treeAfter.pathElements[i] <== pathElements[i]; + } + treeAfter.leaf <== leaf; + treeAfter.root === newRoot; +} diff --git a/circuits/Utils.circom b/circuits/Utils.circom new file mode 100644 index 0000000..977bd2a --- /dev/null +++ b/circuits/Utils.circom @@ -0,0 +1,58 @@ +include "../node_modules/circomlib/circuits/bitify.circom"; +include "../node_modules/circomlib/circuits/sha256/sha256.circom"; + +template TreeUpdateArgsHasher(nLeaves) { + signal private input oldRoot; + signal private input newRoot; + signal private input pathIndices; + signal private input instances[nLeaves]; + signal private input hashes[nLeaves]; + signal private input blocks[nLeaves]; + signal output out; + + var header = 256 + 256 + 32; + var bitsPerLeaf = 160 + 256 + 32; + component hasher = Sha256(header + nLeaves * bitsPerLeaf); + + component bitsOldRoot = Num2Bits(256); + component bitsNewRoot = Num2Bits(256); + component bitsPathIndices = Num2Bits(32); + component bitsInstance[nLeaves]; + component bitsHash[nLeaves]; + component bitsBlock[nLeaves]; + + bitsOldRoot.in <== oldRoot; + bitsNewRoot.in <== newRoot; + bitsPathIndices.in <== pathIndices; + for(var i = 0; i < 256; i++) { + hasher.in[i] <== bitsOldRoot.out[255 - i]; + } + for(var i = 0; i < 256; i++) { + hasher.in[i + 256] <== bitsNewRoot.out[255 - i]; + } + for(var i = 0; i < 32; i++) { + hasher.in[i + 512] <== bitsPathIndices.out[31 - i]; + } + for(var leaf = 0; leaf < nLeaves; leaf++) { + bitsHash[leaf] = Num2Bits(256); + bitsInstance[leaf] = Num2Bits(160); + bitsBlock[leaf] = Num2Bits(32); + bitsHash[leaf].in <== hashes[leaf]; + bitsInstance[leaf].in <== instances[leaf]; + bitsBlock[leaf].in <== blocks[leaf]; + for(var i = 0; i < 256; i++) { + hasher.in[header + leaf * bitsPerLeaf + i] <== bitsHash[leaf].out[255 - i]; + } + for(var i = 0; i < 160; i++) { + hasher.in[header + leaf * bitsPerLeaf + i + 256] <== bitsInstance[leaf].out[159 - i]; + } + for(var i = 0; i < 32; i++) { + hasher.in[header + leaf * bitsPerLeaf + i + 416] <== bitsBlock[leaf].out[31 - i]; + } + } + component b2n = Bits2Num(256); + for (var i = 0; i < 256; i++) { + b2n.in[i] <== hasher.out[255 - i]; + } + out <== b2n.out; +} \ No newline at end of file diff --git a/contracts/Greeter.sol b/contracts/Greeter.sol new file mode 100644 index 0000000..e2b04f5 --- /dev/null +++ b/contracts/Greeter.sol @@ -0,0 +1,23 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.6.0; + +import "hardhat/console.sol"; + + +contract Greeter { + string greeting; + + constructor(string memory _greeting) public { + console.log("Deploying a Greeter with greeting:", _greeting); + greeting = _greeting; + } + + function greet() public view returns (string memory) { + return greeting; + } + + function setGreeting(string memory _greeting) public { + console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); + greeting = _greeting; + } +} diff --git a/contracts/TornadoTrees.sol b/contracts/TornadoTrees.sol new file mode 100644 index 0000000..79821be --- /dev/null +++ b/contracts/TornadoTrees.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +import "torn-token/contracts/ENS.sol"; +import "./interfaces/ITornadoTrees.sol"; +import "./interfaces/IVerifier.sol"; + +contract TornadoTrees is ITornadoTrees, EnsResolve { + address public immutable governance; + bytes32 public depositRoot; + bytes32 public previousDepositRoot; + bytes32 public withdrawalRoot; + bytes32 public previousWithdrawalRoot; + address public tornadoProxy; + IVerifier public immutable treeUpdateVerifier; + + // make sure CHUNK_TREE_HEIGHT has the same value in BatchTreeUpdate.circom and IVerifier.sol + uint256 public constant CHUNK_TREE_HEIGHT = 7; + uint256 public constant CHUNK_SIZE = 2**CHUNK_TREE_HEIGHT; + uint256 public constant ITEM_SIZE = 32 + 20 + 4; + uint256 public constant BYTES_SIZE = 32 + 32 + 4 + CHUNK_SIZE * ITEM_SIZE; + uint256 public constant SNARK_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + bytes32[] public deposits; + uint256 public lastProcessedDepositLeaf; + + bytes32[] public withdrawals; + uint256 public lastProcessedWithdrawalLeaf; + + event DepositData(address instance, bytes32 indexed hash, uint256 block, uint256 index); + event WithdrawalData(address instance, bytes32 indexed hash, uint256 block, uint256 index); + + struct TreeLeaf { + bytes32 hash; + address instance; + uint32 block; + } + + struct Batch { + bytes32 oldRoot; + bytes32 newRoot; + uint8 pathIndices; + TreeLeaf[CHUNK_SIZE] events; + } + + modifier onlyTornadoProxy { + require(msg.sender == tornadoProxy, "Not authorized"); + _; + } + + modifier onlyGovernance() { + require(msg.sender == governance, "Only governance can perform this action"); + _; + } + + constructor( + bytes32 _governance, + bytes32 _tornadoProxy, + bytes32 _treeUpdateVerifier, + bytes32 _depositRoot, + bytes32 _withdrawalRoot + ) public { + governance = resolve(_governance); + tornadoProxy = resolve(_tornadoProxy); + treeUpdateVerifier = IVerifier(resolve(_treeUpdateVerifier)); + depositRoot = _depositRoot; + withdrawalRoot = _withdrawalRoot; + } + + function registerDeposit(address _instance, bytes32 _commitment) external override onlyTornadoProxy { + deposits.push(keccak256(abi.encode(_instance, _commitment, blockNumber()))); + emit DepositData(_instance, _commitment, blockNumber(), deposits.length - 1); + } + + function registerWithdrawal(address _instance, bytes32 _nullifier) external override onlyTornadoProxy { + withdrawals.push(keccak256(abi.encode(_instance, _nullifier, blockNumber()))); + emit WithdrawalData(_instance, _nullifier, blockNumber(), withdrawals.length - 1); + } + + // todo !!! ensure that during migration the tree is filled evenly + function updateDepositTree( + bytes calldata _proof, + bytes32 _argsHash, + bytes32 _currentRoot, + bytes32 _newRoot, + uint32 _pathIndices, + TreeLeaf[CHUNK_SIZE] calldata _events + ) public { + uint256 offset = lastProcessedDepositLeaf; + require(_newRoot != previousDepositRoot, "Outdated deposit root"); + require(_currentRoot == depositRoot, "Proposed deposit root is invalid"); + require(_pathIndices == offset >> CHUNK_TREE_HEIGHT, "Incorrect insert index"); + require(uint256(_newRoot) < SNARK_FIELD, "Proposed root is out of range"); // optional + + bytes memory data = new bytes(BYTES_SIZE); + assembly { + mstore(add(data, 0x44), _pathIndices) + mstore(add(data, 0x40), _newRoot) + mstore(add(data, 0x20), _currentRoot) + } + for (uint256 i = 0; i < CHUNK_SIZE; i++) { + (bytes32 hash, address instance, uint32 depositBlock) = (_events[i].hash, _events[i].instance, _events[i].block); + bytes32 leafHash = keccak256(abi.encode(instance, hash, depositBlock)); + require(leafHash == deposits[offset + i], "Incorrect deposit"); + require(uint256(hash) < SNARK_FIELD, "Hash out of range"); // optional + assembly { + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x7c), depositBlock) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x78), instance) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x64), hash) + } + delete deposits[offset + i]; + } + + uint256 argsHash = uint256(sha256(data)) % SNARK_FIELD; + require(argsHash == uint256(_argsHash), "Invalid args hash"); + require(treeUpdateVerifier.verifyProof(_proof, [argsHash]), "Invalid deposit tree update proof"); + + previousDepositRoot = _currentRoot; + depositRoot = _newRoot; + lastProcessedDepositLeaf = offset + CHUNK_SIZE; + } + + function updateWithdrawalTree( + bytes calldata _proof, + bytes32 _argsHash, + bytes32 _currentRoot, + bytes32 _newRoot, + uint256 _pathIndices, + TreeLeaf[CHUNK_SIZE] calldata _events + ) public { + uint256 offset = lastProcessedWithdrawalLeaf; + require(_newRoot != previousWithdrawalRoot, "Outdated withdrawal root"); + require(_currentRoot == withdrawalRoot, "Proposed withdrawal root is invalid"); + require(_pathIndices == offset >> CHUNK_TREE_HEIGHT, "Incorrect insert index"); + require(uint256(_newRoot) < SNARK_FIELD, "Proposed root is out of range"); + + bytes memory data = new bytes(BYTES_SIZE); + assembly { + mstore(add(data, 0x44), _pathIndices) + mstore(add(data, 0x40), _newRoot) + mstore(add(data, 0x20), _currentRoot) + } + for (uint256 i = 0; i < CHUNK_SIZE; i++) { + (bytes32 hash, address instance, uint32 withdrawalBlock) = (_events[i].hash, _events[i].instance, _events[i].block); + bytes32 leafHash = keccak256(abi.encode(instance, hash, withdrawalBlock)); + require(leafHash == withdrawals[offset + i], "Incorrect withdrawal"); + require(uint256(hash) < SNARK_FIELD, "Hash out of range"); + assembly { + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x7c), withdrawalBlock) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x78), instance) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x64), hash) + } + delete withdrawals[offset + i]; + } + + uint256 argsHash = uint256(sha256(data)) % SNARK_FIELD; + require(argsHash == uint256(_argsHash), "Invalid args hash"); + require(treeUpdateVerifier.verifyProof(_proof, [argsHash]), "Invalid withdrawal tree update proof"); + + previousWithdrawalRoot = _currentRoot; + withdrawalRoot = _newRoot; + lastProcessedWithdrawalLeaf = offset + CHUNK_SIZE; + } + + function validateRoots(bytes32 _depositRoot, bytes32 _withdrawalRoot) public view { + require(_depositRoot == depositRoot || _depositRoot == previousDepositRoot, "Incorrect deposit tree root"); + require(_withdrawalRoot == withdrawalRoot || _withdrawalRoot == previousWithdrawalRoot, "Incorrect withdrawal tree root"); + } + + function getRegisteredDeposits() external view returns (uint256 count, bytes32[] memory _deposits) { + count = deposits.length - lastProcessedDepositLeaf; + _deposits = new bytes32[](count); + for (uint256 i = 0; i < count; i++) { + _deposits[i] = deposits[lastProcessedDepositLeaf + i]; + } + } + + function getRegisteredWithdrawals() external view returns (uint256 count, bytes32[] memory _withdrawals) { + count = withdrawals.length - lastProcessedWithdrawalLeaf; + _withdrawals = new bytes32[](count); + for (uint256 i = 0; i < count; i++) { + _withdrawals[i] = withdrawals[lastProcessedWithdrawalLeaf + i]; + } + } + + function setTornadoProxyContract(address _tornadoProxy) external onlyGovernance { + tornadoProxy = _tornadoProxy; + } + + function blockNumber() public view virtual returns (uint256) { + return block.number; + } +} diff --git a/contracts/interfaces/ITornadoTrees.sol b/contracts/interfaces/ITornadoTrees.sol new file mode 100644 index 0000000..df571e6 --- /dev/null +++ b/contracts/interfaces/ITornadoTrees.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +interface ITornadoTrees { + function registerDeposit(address instance, bytes32 commitment) external; + + function registerWithdrawal(address instance, bytes32 nullifier) external; +} diff --git a/contracts/interfaces/IVerifier.sol b/contracts/interfaces/IVerifier.sol new file mode 100644 index 0000000..9a52a0f --- /dev/null +++ b/contracts/interfaces/IVerifier.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +interface IVerifier { + function verifyProof(bytes calldata proof, uint256[1] calldata input) external view returns (bool); +} diff --git a/contracts/mocks/Pack.sol b/contracts/mocks/Pack.sol new file mode 100644 index 0000000..d2d5445 --- /dev/null +++ b/contracts/mocks/Pack.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +contract Pack { + + uint256 public constant CHUNK_TREE_HEIGHT = 7; + uint256 public constant CHUNK_SIZE = 2**CHUNK_TREE_HEIGHT; + uint256 public constant ITEM_SIZE = 32 + 20 + 4; + uint256 public constant BYTES_SIZE = CHUNK_SIZE * ITEM_SIZE; + + uint256 public gas1; + uint256 public gas2; + uint256 public gas3; + uint256 public gas4; + bytes32 public hash; + + event DepositData(address instance, bytes32 indexed hash, uint256 block, uint256 index); + + function pack2(bytes32[CHUNK_SIZE] memory hashes, address[CHUNK_SIZE] memory instances, uint32[CHUNK_SIZE] memory blocks) public { + uint256 gasBefore = gasleft(); + bytes memory data = new bytes(BYTES_SIZE); + for(uint i = 0; i < CHUNK_SIZE; i++) { + (bytes32 hash, address instance, uint32 block) = (hashes[i], instances[i], blocks[i]); + assembly { + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x38), block) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x34), instance) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x20), hash) + } + } + uint256 gasHash = gasleft(); + bytes32 hash1 = sha256(data); + uint256 gasEvents = gasleft(); + for(uint i = 0; i < CHUNK_SIZE; i++) { + emit DepositData(instances[i], hashes[i], blocks[i], i); + } + gas1 = gasEvents - gasleft(); + gas2 = gasHash - gasEvents; + gas3 = gasBefore - gasHash; + gas4 = gasBefore; + hash = hash1; + } + + function pack3(bytes32[CHUNK_SIZE] memory hashes, address[CHUNK_SIZE] memory instances, uint32[CHUNK_SIZE] memory blocks) public view returns(uint256 gas1, uint256 gas2, bytes32 hash) { + uint256 gasBefore = gasleft(); + bytes memory data = new bytes(BYTES_SIZE); + for(uint i = 0; i < CHUNK_SIZE; i++) { + (bytes32 hash, address instance, uint32 block) = (hashes[i], instances[i], blocks[i]); + assembly { + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x38), block) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x34), instance) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x20), hash) + } + } + uint256 gasHash = gasleft(); + bytes32 hash = sha256(data); + return (gasleft() - gasHash, gasHash - gasBefore, hash); + } +} \ No newline at end of file diff --git a/contracts/mocks/TornadoTreesMock.sol b/contracts/mocks/TornadoTreesMock.sol new file mode 100644 index 0000000..e98fc82 --- /dev/null +++ b/contracts/mocks/TornadoTreesMock.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +import "../TornadoTrees.sol"; + +contract TornadoTreesMock is TornadoTrees { + uint256 public currentBlock; + + constructor( + bytes32 _governance, + bytes32 _tornadoProxy, + bytes32 _treeUpdateVerifier, + bytes32 _depositRoot, + bytes32 _withdrawalRoot + ) public TornadoTrees(_governance, _tornadoProxy, _treeUpdateVerifier, _depositRoot, _withdrawalRoot) {} + + function resolve(bytes32 _addr) public view override returns (address) { + return address(uint160(uint256(_addr) >> (12 * 8))); + } + + function setBlockNumber(uint256 _blockNumber) public { + currentBlock = _blockNumber; + } + + function blockNumber() public view override returns (uint256) { + return currentBlock == 0 ? block.number : currentBlock; + } + + function register( + address _instance, + bytes32 _commitment, + bytes32 _nullifier, + uint256 _depositBlockNumber, + uint256 _withdrawBlockNumber + ) public { + setBlockNumber(_depositBlockNumber); + deposits.push(keccak256(abi.encode(_instance, _commitment, blockNumber()))); + setBlockNumber(_withdrawBlockNumber); + withdrawals.push(keccak256(abi.encode(_instance, _nullifier, blockNumber()))); + } + + function updateDepositTreeMock( + bytes32 _oldRoot, + bytes32 _newRoot, + uint32 _pathIndices, + TreeLeaf[] calldata _events + ) public pure returns (uint256) { + bytes memory data = new bytes(BYTES_SIZE); + assembly { + mstore(add(data, 0x44), _pathIndices) + mstore(add(data, 0x40), _newRoot) + mstore(add(data, 0x20), _oldRoot) + } + for (uint256 i = 0; i < CHUNK_SIZE; i++) { + (bytes32 hash, address instance, uint32 depositBlock) = (_events[i].hash, _events[i].instance, _events[i].block); + assembly { + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x7c), depositBlock) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x78), instance) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x64), hash) + } + } + return uint256(sha256(data)) % SNARK_FIELD; + } + + function updateDepositTreeMock2( + bytes32 _oldRoot, + bytes32 _newRoot, + uint32 _pathIndices, + TreeLeaf[] calldata _events + ) public pure returns (bytes memory) { + bytes memory data = new bytes(BYTES_SIZE); + assembly { + mstore(add(data, 0x44), _pathIndices) + mstore(add(data, 0x40), _newRoot) + mstore(add(data, 0x20), _oldRoot) + } + for (uint256 i = 0; i < CHUNK_SIZE; i++) { + (bytes32 hash, address instance, uint32 depositBlock) = (_events[i].hash, _events[i].instance, _events[i].block); + assembly { + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x7c), depositBlock) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x78), instance) + mstore(add(add(data, mul(ITEM_SIZE, i)), 0x64), hash) + } + } + return data; + } +} diff --git a/contracts/verifiers/BatchTreeUpdateVerifier.sol b/contracts/verifiers/BatchTreeUpdateVerifier.sol new file mode 100644 index 0000000..d2eef74 --- /dev/null +++ b/contracts/verifiers/BatchTreeUpdateVerifier.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +library Pairing { + uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + + struct G1Point { + uint256 X; + uint256 Y; + } + + // Encoding of field elements is: X[0] * z + X[1] + struct G2Point { + uint256[2] X; + uint256[2] Y; + } + + /* + * @return The negation of p, i.e. p.plus(p.negate()) should be zero + */ + function negate(G1Point memory p) internal pure returns (G1Point memory) { + // The prime q in the base field F_q for G1 + if (p.X == 0 && p.Y == 0) { + return G1Point(0, 0); + } else { + return G1Point(p.X, PRIME_Q - (p.Y % PRIME_Q)); + } + } + + /* + * @return r the sum of two points of G1 + */ + function plus( + G1Point memory p1, + G1Point memory p2 + ) internal view returns (G1Point memory r) { + uint256[4] memory input = [ + p1.X, p1.Y, + p2.X, p2.Y + ]; + bool success; + + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 6, input, 0xc0, r, 0x60) + // Use "invalid" to make gas estimation work + switch success case 0 { invalid() } + } + + require(success, "pairing-add-failed"); + } + + /* + * @return r the product of a point on G1 and a scalar, i.e. + * p == p.scalarMul(1) and p.plus(p) == p.scalarMul(2) for all + * points p. + */ + function scalarMul(G1Point memory p, uint256 s) internal view returns (G1Point memory r) { + uint256[3] memory input = [p.X, p.Y, s]; + bool success; + + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 7, input, 0x80, r, 0x60) + // Use "invalid" to make gas estimation work + switch success case 0 { invalid() } + } + + require(success, "pairing-mul-failed"); + } + + /* @return The result of computing the pairing check + * e(p1[0], p2[0]) * .... * e(p1[n], p2[n]) == 1 + * For example, + * pairing([P1(), P1().negate()], [P2(), P2()]) should return true. + */ + function pairing( + G1Point memory a1, + G2Point memory a2, + G1Point memory b1, + G2Point memory b2, + G1Point memory c1, + G2Point memory c2, + G1Point memory d1, + G2Point memory d2 + ) internal view returns (bool) { + uint256[24] memory input = [ + a1.X, a1.Y, a2.X[0], a2.X[1], a2.Y[0], a2.Y[1], + b1.X, b1.Y, b2.X[0], b2.X[1], b2.Y[0], b2.Y[1], + c1.X, c1.Y, c2.X[0], c2.X[1], c2.Y[0], c2.Y[1], + d1.X, d1.Y, d2.X[0], d2.X[1], d2.Y[0], d2.Y[1] + ]; + uint256[1] memory out; + bool success; + + // solium-disable-next-line security/no-inline-assembly + assembly { + success := staticcall(sub(gas(), 2000), 8, input, mul(24, 0x20), out, 0x20) + // Use "invalid" to make gas estimation work + switch success case 0 { invalid() } + } + + require(success, "pairing-opcode-failed"); + return out[0] != 0; + } +} + +contract BatchTreeUpdateVerifier { + uint256 constant SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + uint256 constant PRIME_Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; + using Pairing for *; + + struct VerifyingKey { + Pairing.G1Point alfa1; + Pairing.G2Point beta2; + Pairing.G2Point gamma2; + Pairing.G2Point delta2; + Pairing.G1Point[2] IC; + } + + function verifyingKey() internal pure returns (VerifyingKey memory vk) { + vk.alfa1 = Pairing.G1Point(uint256(20475789791681002364587166738311620805815985969091106757478379420262430093495), uint256(3034180384279528157431123624668892018871098425968640214767822771352219138078)); + vk.beta2 = Pairing.G2Point([uint256(347992840312110670849483472224503623225781749273259516677464742758581199694), uint256(16853081403278411985324640353650047676779142117029386935051386044282804346484)], [uint256(10461241566647602546027012417757263991485755060136522105605550609788790829933), uint256(16049761706815422591462572571571264938897676292217555774707799384732883004386)]); + vk.gamma2 = Pairing.G2Point([uint256(5535450215937949788522672716791294482208969162172756729752675877422249461391), uint256(4537903555000997751027892507073556632992848536024556182449526590439971414042)], [uint256(6688278057604431581483695896713912024597719708930089928002132340517626404891), uint256(15745439923152020754042431613052318298038129099865656040309120795605091105487)]); + vk.delta2 = Pairing.G2Point([uint256(10712491908603553476637447918495381165104059355722416702328240143919146641319), uint256(15855442659923189569787773688895011287546687523233653745264460947047886121140)], [uint256(18278088599243830423965796542892879791365910862597475788753708589843343437901), uint256(10765606859348375283724614934374540130725132299795942405716724739350245709734)]); + vk.IC[0] = Pairing.G1Point(uint256(18147360875100520747353841225428915644191762631193821400291387675910597374366), uint256(17222433096548585553756828362569506045947134360392537102794184064340219776032)); + vk.IC[1] = Pairing.G1Point(uint256(3514632146136652297064638325657684436433185732623721288055192259268961814948), uint256(8363257337389338977321440370428118205387545635573906956020792115766452976369)); + + } + + /* + * @returns Whether the proof is valid given the hardcoded verifying key + * above and the public inputs + */ + function verifyProof( + bytes memory proof, + uint256[1] memory input + ) public view returns (bool) { + uint256[8] memory p = abi.decode(proof, (uint256[8])); + for (uint8 i = 0; i < p.length; i++) { + // Make sure that each element in the proof is less than the prime q + require(p[i] < PRIME_Q, "verifier-proof-element-gte-prime-q"); + } + Pairing.G1Point memory proofA = Pairing.G1Point(p[0], p[1]); + Pairing.G2Point memory proofB = Pairing.G2Point([p[2], p[3]], [p[4], p[5]]); + Pairing.G1Point memory proofC = Pairing.G1Point(p[6], p[7]); + + VerifyingKey memory vk = verifyingKey(); + // Compute the linear combination vkX + Pairing.G1Point memory vkX = vk.IC[0]; + for (uint256 i = 0; i < input.length; i++) { + // Make sure that every input is less than the snark scalar field + require(input[i] < SNARK_SCALAR_FIELD, "verifier-input-gte-snark-scalar-field"); + vkX = Pairing.plus(vkX, Pairing.scalarMul(vk.IC[i + 1], input[i])); + } + + return Pairing.pairing( + Pairing.negate(proofA), + proofB, + vk.alfa1, + vk.beta2, + vkX, + vk.gamma2, + proofC, + vk.delta2 + ); + } +} + diff --git a/hardhat.config.js b/hardhat.config.js new file mode 100644 index 0000000..20a3ef7 --- /dev/null +++ b/hardhat.config.js @@ -0,0 +1,22 @@ +require("@nomiclabs/hardhat-waffle"); + +// This is a sample Hardhat task. To learn how to create your own go to +// https://hardhat.org/guides/create-task.html +task("accounts", "Prints the list of accounts", async () => { + const accounts = await ethers.getSigners(); + + for (const account of accounts) { + console.log(account.address); + } +}); + +// You need to export an object to set up your config +// Go to https://hardhat.org/config/ to learn more + +/** + * @type import('hardhat/config').HardhatUserConfig + */ +module.exports = { + solidity: "0.6.12", +}; + diff --git a/optimize/Dockerfile b/optimize/Dockerfile new file mode 100644 index 0000000..753daec --- /dev/null +++ b/optimize/Dockerfile @@ -0,0 +1,47 @@ +FROM ubuntu + +RUN apt-get update && \ + apt-get install -y python3 python3-distutils g++ make curl git && \ + rm -rf /var/lib/apt/lists/* + +# Install nvm with node and npm +RUN rm /bin/sh && ln -s /bin/bash /bin/sh +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION 14.8.0 +RUN curl https://raw.githubusercontent.com/creationix/nvm/v0.30.1/install.sh | bash \ + && source $NVM_DIR/nvm.sh \ + && nvm install $NODE_VERSION \ + && nvm alias default $NODE_VERSION \ + && nvm use default +ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules +ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH +RUN node --version + +WORKDIR /root + +RUN git clone https://github.com/nodejs/node.git +RUN git clone https://github.com/iden3/circom.git + +COPY node.sh /tmp + +RUN apt-get update && apt-get install -y ninja-build +RUN /tmp/node.sh + +RUN cd circom && \ + git checkout v0.5.35 && \ + npm install + +RUN git clone https://github.com/iden3/r1csoptimize +RUN cd r1csoptimize && \ + git checkout 8bc528b06c0f98818d1b5224e2078397f0bb7faf && \ + npm install + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +RUN source $HOME/.cargo/env && cargo install zkutil +RUN npm install -g circom snarkjs + +WORKDIR /root/test +RUN npm init -y && npm install circomlib +RUN apt-get update && apt-get install -y ne +RUN mkdir circuits +COPY sha/circuit.circom sha/input.js test.sh ./circuits/ diff --git a/optimize/node.sh b/optimize/node.sh new file mode 100755 index 0000000..5428ba0 --- /dev/null +++ b/optimize/node.sh @@ -0,0 +1,34 @@ +#!/bin/bash -e +cd node +git checkout 8beef5eeb82425b13d447b50beafb04ece7f91b1 +patch -p1 <(this)->heap(); + if (heap->gc_state() != i::Heap::NOT_IN_GC) return; +- heap->ReportExternalMemoryPressure(); ++ // heap->ReportExternalMemoryPressure(); + } + + HeapProfiler* Isolate::GetHeapProfiler() { +diff --git a/deps/v8/src/objects/backing-store.cc b/deps/v8/src/objects/backing-store.cc +index bd9f39b7d3..c7d7e58ef3 100644 +--- a/deps/v8/src/objects/backing-store.cc ++++ b/deps/v8/src/objects/backing-store.cc +@@ -34,7 +34,7 @@ constexpr bool kUseGuardRegions = false; + // address space limits needs to be smaller. + constexpr size_t kAddressSpaceLimit = 0x8000000000L; // 512 GiB + #elif V8_TARGET_ARCH_64_BIT +-constexpr size_t kAddressSpaceLimit = 0x10100000000L; // 1 TiB + 4 GiB ++constexpr size_t kAddressSpaceLimit = 0x40100000000L; // 4 TiB + 4 GiB + #else + constexpr size_t kAddressSpaceLimit = 0xC0000000; // 3 GiB + #endif +EOL +# ./configure --ninja +# JOBS=24 make +./configure +make -j12 \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..213a7e7 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "hardhat-project", + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.0.1", + "@nomiclabs/hardhat-waffle": "^2.0.1", + "babel-eslint": "^10.1.0", + "chai": "^4.2.0", + "eslint": "^7.19.0", + "eslint-config-prettier": "^7.2.0", + "eslint-plugin-prettier": "^3.3.1", + "ethereum-waffle": "^3.2.2", + "ethers": "^5.0.26", + "hardhat": "^2.0.8", + "prettier": "^2.2.1", + "prettier-plugin-solidity": "^1.0.0-beta.3", + "solhint-plugin-prettier": "^0.0.5", + "torn-token": "^1.0.0" + }, + "dependencies": { + "circom": "^0.5.38", + "circom_runtime": "^0.1.12", + "circomlib": "^0.4.1", + "dotenv": "^8.2.0", + "ffiasm": "^0.1.1", + "fixed-merkle-tree": "^0.5.0", + "jssha": "^3.2.0", + "snarkjs": "^0.3.57", + "tmp-promise": "^3.0.2" + } +} diff --git a/scripts/buildCircuit.sh b/scripts/buildCircuit.sh new file mode 100755 index 0000000..f95a2b7 --- /dev/null +++ b/scripts/buildCircuit.sh @@ -0,0 +1,10 @@ +#!/bin/bash -e +if [ "$2" = "large" ]; then + npx circom -v -f -r build/circuits/$1.r1cs -c build/circuits/$1.cpp -s build/circuits/$1.sym circuits/$1.circom +else + npx circom -v -r build/circuits/$1.r1cs -w build/circuits/$1.wasm -s build/circuits/$1.sym circuits/$1.circom +fi +zkutil setup -c build/circuits/$1.r1cs -p build/circuits/$1.params +zkutil generate-verifier -p build/circuits/$1.params -v build/circuits/${1}Verifier.sol +sed -i.bak "s/contract Verifier/contract ${1}Verifier/g" build/circuits/${1}Verifier.sol +npx snarkjs info -r build/circuits/$1.r1cs diff --git a/scripts/buildWitness.sh b/scripts/buildWitness.sh new file mode 100755 index 0000000..801f162 --- /dev/null +++ b/scripts/buildWitness.sh @@ -0,0 +1,8 @@ +#!/bin/bash -e +# required dependencies: libgmp-dev nlohmann-json3-dev nasm g++ +cd build/circuits +node ../../node_modules/ffiasm/src/buildzqfield.js -q 21888242871839275222246405745257275088548364400416034343698204186575808495617 -n Fr +nasm -felf64 fr.asm +cp ../../node_modules/circom_runtime/c/*.cpp ./ +cp ../../node_modules/circom_runtime/c/*.hpp ./ +g++ -pthread main.cpp calcwit.cpp utils.cpp fr.cpp fr.o ${1}.cpp -o ${1} -lgmp -std=c++11 -O3 -fopenmp -DSANITY_CHECK \ No newline at end of file diff --git a/scripts/sample-script.js b/scripts/sample-script.js new file mode 100644 index 0000000..1aa1a24 --- /dev/null +++ b/scripts/sample-script.js @@ -0,0 +1,32 @@ +// We require the Hardhat Runtime Environment explicitly here. This is optional +// but useful for running the script in a standalone fashion through `node