Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mocked mode and coverage #31

Merged
merged 1 commit into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ dist
node_modules
types
deployments
kms-fhe-keys/
network-fhe-keys/
fhevmTemp/
abi/

# files
*.env
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ coverage.json
package-lock.json
pnpm-lock.yaml
yarn.lock
README.md
immortal-tofu marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 5 additions & 1 deletion .solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ module.exports = {
providerOptions: {
mnemonic: process.env.MNEMONIC,
},
skipFiles: ["test"],
skipFiles: ["test", "fhevmTemp"],
mocha: {
fgrep: "[skip-on-coverage]",
invert: true,
},
};
42 changes: 40 additions & 2 deletions CustomProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,55 @@ import { ethers } from "ethers";
import { ProviderWrapper } from "hardhat/plugins";

class CustomProvider extends ProviderWrapper {
public lastBlockSnapshot: number;
public lastCounterRand: number;
public lastBlockSnapshotForDecrypt: number;

constructor(protected readonly _wrappedProvider: any) {
super(_wrappedProvider);
this.lastBlockSnapshot = 0; // Initialize the variable
this.lastCounterRand = 0;
this.lastBlockSnapshotForDecrypt = 0;
}

public async request(args: { method: string; params?: any[] }): Promise<any> {
async request(args: { method: string; params?: any[] }) {
if (args.method === "eth_estimateGas") {
const estimatedGasLimit = BigInt(await this._wrappedProvider.request(args));
const increasedGasLimit = ethers.toBeHex((estimatedGasLimit * 120n) / 100n); // override estimated gasLimit by 120%, to avoid some edge case with ethermint gas estimation
return increasedGasLimit;
}
return this._wrappedProvider.request(args);
if (args.method === "evm_revert") {
const result = await this._wrappedProvider.request(args);
const blockNumberHex = await this._wrappedProvider.request({ method: "eth_blockNumber" });
this.lastBlockSnapshot = parseInt(blockNumberHex);
this.lastBlockSnapshotForDecrypt = parseInt(blockNumberHex);

const callData = {
to: "0x000000000000000000000000000000000000005d",
data: "0x1f20d85c",
};
this.lastCounterRand = await this._wrappedProvider.request({
method: "eth_call",
params: [callData, "latest"],
});
return result;
}
if (args.method === "get_lastBlockSnapshot") {
return [this.lastBlockSnapshot, this.lastCounterRand];
}
if (args.method === "get_lastBlockSnapshotForDecrypt") {
return this.lastBlockSnapshotForDecrypt;
}
if (args.method === "set_lastBlockSnapshot") {
this.lastBlockSnapshot = args.params![0];
return this.lastBlockSnapshot;
}
if (args.method === "set_lastBlockSnapshotForDecrypt") {
this.lastBlockSnapshotForDecrypt = args.params![0];
return this.lastBlockSnapshotForDecrypt;
}
const result = this._wrappedProvider.request(args);
return result;
}
}

Expand Down
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,15 @@ Install [docker](https://docs.docker.com/engine/install/)
Install [pnpm](https://pnpm.io/installation)

Before being able to run any command, you need to create a `.env` file and set a BIP-39 compatible mnemonic as an
environment variable. You can follow the example in `.env.example`. If you don't already have a mnemonic, you can use
this [website](https://iancoleman.io/bip39/) to generate one.
environment variable. You can follow the example in `.env.example` and start with the following command:

Then, proceed with installing dependencies - please make sure to use Node v20 or more recent or this will fail:
```sh
cp .env.example .env
```

If you don't already have a mnemonic, you can use this [website](https://iancoleman.io/bip39/) to generate one.

Then, proceed with installing dependencies - please **_make sure to use Node v20_** or more recent or this will fail:

```sh
pnpm install
Expand Down Expand Up @@ -195,14 +200,27 @@ pnpm clean

### Mocked mode

**Warning** Since upgrading fhevm to v0.5 the previous version of mocked mode is no longer functional. We are now
working on a new version of the mocked mode which is more faithful to the real fhevm, with (almost) full parity to fhevm
functionalities. This is now possible since fhevm v0.5 introduced an explicit ACL contract. New mocked feature will be
delivered very soon.

The mocked mode allows faster testing and the ability to analyze coverage of the tests. In this mocked version,
encrypted types are not really encrypted, and the tests are run on the original version of the EVM, on a local hardhat
network instance.
network instance. To run the tests in mocked mode, you can use directly the following command:

```bash
pnpm test:mock
```

To analyze the coverage of the tests (in mocked mode necessarily, as this cannot be done on the real fhEVM node), you
can use this command :

```bash
pnpm coverage:mock
```

Then open the file `coverage/index.html`. You can see there which line or branch for each contract which has been
covered or missed by your test suite. This allows increased security by pointing out missing branches not covered yet by
the current tests.

> [!Note]
> Due to intrinsic limitations of the original EVM, the mocked version differ in few corner cases from the real fhEVM, the main difference is the difference in gas prices for the FHE operations. This means that before deploying to production, developers still need to run the tests with the original fhEVM node, as a final check in non-mocked mode, with `pnpm test`.

### Syntax Highlighting

Expand Down
127 changes: 38 additions & 89 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import "@nomicfoundation/hardhat-toolbox";
import dotenv from "dotenv";
import * as fs from "fs";
import * as fs from "fs-extra";
import "hardhat-deploy";
import "hardhat-ignore-warnings";
import "hardhat-preprocessor";
import { TASK_PREPROCESS } from "hardhat-preprocessor";
import type { HardhatUserConfig, extendProvider } from "hardhat/config";
import { task } from "hardhat/config";
import type { NetworkUserConfig } from "hardhat/types";
import { resolve } from "path";
import * as path from "path";

import CustomProvider from "./CustomProvider";
// Adjust the import path as needed
import "./tasks/accounts";
import "./tasks/deployERC20";
import "./tasks/getEthereumAddress";
import "./tasks/taskDeploy";
import "./tasks/taskGatewayRelayer";
Expand All @@ -24,18 +22,6 @@ extendProvider(async (provider, config, network) => {
return newProvider;
});

function getAllSolidityFiles(dir: string, fileList: string[] = []): string[] {
fs.readdirSync(dir).forEach((file) => {
const filePath = path.join(dir, file);
if (fs.statSync(filePath).isDirectory()) {
getAllSolidityFiles(filePath, fileList);
} else if (filePath.endsWith(".sol")) {
fileList.push(filePath);
}
});
return fileList;
}

task("compile:specific", "Compiles only the specified contract")
.addParam("contract", "The contract's path")
.setAction(async ({ contract }, hre) => {
Expand All @@ -45,43 +31,15 @@ task("compile:specific", "Compiles only the specified contract")
await hre.run("compile");
});

task("coverage-mock", "Run coverage after running pre-process task").setAction(async function (args, env) {
const contractsPath = path.join(env.config.paths.root, "contracts/");
const solidityFiles = getAllSolidityFiles(contractsPath);
const originalContents: Record<string, string> = {};
solidityFiles.forEach((filePath) => {
originalContents[filePath] = fs.readFileSync(filePath, { encoding: "utf8" });
});

try {
await env.run(TASK_PREPROCESS);
await env.run("coverage");
} finally {
// Restore original files
for (const filePath in originalContents) {
fs.writeFileSync(filePath, originalContents[filePath], { encoding: "utf8" });
}
}
});

const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || "./.env";
dotenv.config({ path: resolve(__dirname, dotenvConfigPath) });

// Ensure that we have all the environment variables we need.
const mnemonic: string | undefined = process.env.MNEMONIC;
if (!mnemonic) {
throw new Error("Please set your MNEMONIC in a .env file");
}

const network = process.env.HARDHAT_NETWORK;

function getRemappings() {
return fs
.readFileSync("remappings.txt", "utf8")
.split("\n")
.filter(Boolean) // remove empty lines
.map((line: string) => line.trim().split("="));
}

const chainIds = {
zama: 8009,
local: 9000,
Expand Down Expand Up @@ -116,59 +74,50 @@ function getChainConfig(chain: keyof typeof chainIds): NetworkUserConfig {
};
}

task("coverage").setAction(async (taskArgs, hre, runSuper) => {
hre.config.networks.hardhat.allowUnlimitedContractSize = true;
hre.config.networks.hardhat.blockGasLimit = 1099511627775;
await runSuper(taskArgs);
});

task("test", async (taskArgs, hre, runSuper) => {
// Run modified test task

if (network === "hardhat") {
if (hre.network.name === "hardhat") {
// in fhevm mode all this block is done when launching the node via `pnmp fhevm:start`
const privKeyDeployer = process.env.PRIVATE_KEY_ORACLE_DEPLOYER;
const privKeyOwner = process.env.PRIVATE_KEY_ORACLE_OWNER;
const privKeyRelayer = process.env.PRIVATE_KEY_ORACLE_RELAYER;
const deployerAddress = new hre.ethers.Wallet(privKeyDeployer!).address;
const ownerAddress = new hre.ethers.Wallet(privKeyOwner!).address;
const relayerAddress = new hre.ethers.Wallet(privKeyRelayer!).address;

await hre.run("clean");
await hre.run("compile:specific", { contract: "contracts" });
const sourceDir = path.resolve(__dirname, "node_modules/fhevm/");
const destinationDir = path.resolve(__dirname, "fhevmTemp/");
fs.copySync(sourceDir, destinationDir, { dereference: true });
await hre.run("compile:specific", { contract: "fhevmTemp/lib" });
await hre.run("compile:specific", { contract: "fhevmTemp/gateway" });
const abiDir = path.resolve(__dirname, "abi");
fs.ensureDirSync(abiDir);
const sourceFile = path.resolve(__dirname, "artifacts/fhevmTemp/lib/TFHEExecutor.sol/TFHEExecutor.json");
const destinationFile = path.resolve(abiDir, "TFHEExecutor.json");
fs.copyFileSync(sourceFile, destinationFile);

const targetAddress = "0x000000000000000000000000000000000000005d";
const NeverRevert = await hre.artifacts.readArtifact("MockedPrecompile");
const bytecode = NeverRevert.deployedBytecode;
await hre.network.provider.send("hardhat_setCode", [targetAddress, bytecode]);
console.log(`Code of Mocked Pre-compile set at address: ${targetAddress}`);
fs.removeSync("fhevmTemp/");

const privKeyDeployer = process.env.PRIVATE_KEY_GATEWAY_DEPLOYER;
await hre.run("task:computePredeployAddress", { privateKey: privKeyDeployer });

const bal = "0x1000000000000000000000000000000000000000";
const p1 = hre.network.provider.send("hardhat_setBalance", [deployerAddress, bal]);
const p2 = hre.network.provider.send("hardhat_setBalance", [ownerAddress, bal]);
const p3 = hre.network.provider.send("hardhat_setBalance", [relayerAddress, bal]);
await Promise.all([p1, p2, p3]);
await hre.run("compile");
await hre.run("task:deployOracle", { privateKey: privKeyDeployer, ownerAddress: ownerAddress });

const parsedEnv = dotenv.parse(fs.readFileSync("node_modules/fhevm/oracle/.env.oracle"));
const oraclePredeployAddress = parsedEnv.ORACLE_CONTRACT_PREDEPLOY_ADDRESS;

await hre.run("task:addRelayer", {
privateKey: privKeyOwner,
oracleAddress: oraclePredeployAddress,
relayerAddress: relayerAddress,
});
await hre.run("task:computeACLAddress");
await hre.run("task:computeTFHEExecutorAddress");
await hre.run("task:computeKMSVerifierAddress");
await hre.run("task:deployACL");
await hre.run("task:deployTFHEExecutor");
await hre.run("task:deployKMSVerifier");
await hre.run("task:launchFhevm", { skipGetCoin: false });
}

await runSuper();
});

const config: HardhatUserConfig = {
preprocess: {
eachLine: () => ({
transform: (line: string) => {
if (network === "hardhat") {
if (line.match(/".*.sol";$/)) {
for (const [from, to] of getRemappings()) {
if (line.includes(from)) {
line = line.replace(from, to);
break;
}
}
}
}
return line;
},
}),
},
defaultNetwork: "local",
namedAccounts: {
deployer: 0,
Expand Down
11 changes: 8 additions & 3 deletions launch-fhevm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
# 1. A local and **fresh** fhEVM node is already running.
# 2. All test addresses are funded (e.g. via the fund_test_addresses.sh script).
npx hardhat clean
mkdir fhevmTemp
mkdir -p fhevmTemp
cp -L -r node_modules/fhevm fhevmTemp/
npx hardhat compile:specific --contract fhevmTemp/fhevm/lib
npx hardhat compile:specific --contract fhevmTemp/fhevm/gateway
rm -rf fhevmTemp
mkdir -p abi
cp artifacts/fhevmTemp/fhevm/lib/TFHEExecutor.sol/TFHEExecutor.json abi/TFHEExecutor.json

PRIVATE_KEY_GATEWAY_DEPLOYER=$(grep PRIVATE_KEY_GATEWAY_DEPLOYER .env | cut -d '"' -f 2)
npx hardhat task:computePredeployAddress --private-key "$PRIVATE_KEY_GATEWAY_DEPLOYER"

npx hardhat compile:specific --contract contracts

npx hardhat task:computeACLAddress
npx hardhat task:computeTFHEExecutorAddress
npx hardhat task:computeKMSVerifierAddress
npx hardhat task:deployACL
npx hardhat task:deployTFHEExecutor
npx hardhat task:deployKMSVerifier

npx hardhat task:launchFhevm --skip-get-coin true
rm -rf fhevmTemp

npx hardhat task:launchFhevm --skip-get-coin true
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"ethers": "^6.8.0",
"fhevm": "^0.5.3",
"fhevm": "^0.5.5",
"fhevmjs": "^0.5.2",
"fs-extra": "^10.1.0",
"hardhat": "^2.19.4",
"hardhat": "^2.22.8",
"hardhat-deploy": "^0.11.29",
"hardhat-gas-reporter": "^1.0.9",
"hardhat-ignore-warnings": "^0.2.11",
"hardhat-preprocessor": "^0.1.5",
"lodash": "^4.17.21",
"mocha": "^10.1.0",
Expand All @@ -45,7 +46,7 @@
"rimraf": "^4.1.2",
"solhint": "^3.4.0",
"solhint-plugin-prettier": "^0.0.5",
"solidity-coverage": "0.8.6",
"solidity-coverage": "0.8.12",
"ts-generator": "^0.1.1",
"ts-node": "^10.9.1",
"typechain": "^8.2.0",
Expand Down Expand Up @@ -81,8 +82,8 @@
"prettier:write": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"",
"typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain",
"test": "hardhat test",
"test:mock": "HARDHAT_NETWORK=hardhat hardhat test --network hardhat",
"coverage:mock": "HARDHAT_NETWORK=hardhat hardhat coverage-mock --network hardhat",
"test:mock": "hardhat test --network hardhat",
"coverage:mock": "hardhat coverage",
"task:getEthereumAddress": "hardhat task:getEthereumAddress",
"task:deployERC20": "hardhat task:deployERC20",
"task:accounts": "hardhat task:accounts",
Expand All @@ -97,6 +98,7 @@
"fhevm:faucet:eve": "docker exec -i zama-dev-fhevm-validator-1 faucet $(npx hardhat task:getEthereumAddressEve)"
},
"dependencies": {
"hardhat-ignore-warnings": "^0.2.11"
"extra-bigint": "^1.1.18",
"sqlite3": "^5.1.7"
}
}
}
Loading
Loading