Skip to content

Commit

Permalink
feat: gate sizing implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
AlanVerbner committed Sep 27, 2023
1 parent 5380581 commit fbd3837
Show file tree
Hide file tree
Showing 7 changed files with 1,151 additions and 542 deletions.
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@
This repository contains a set of utilities for testing Mina zkapps:

- Custom jest matchers
- An interactive debugger to interact with locally deployed contracts
- A contract (gates) sizer

# Requirements

- `node +18` (nvm recommended)
- `[email protected].*`

# Installing

In order to use this tool, you should install it as a dev dependency on your existing project, for example, one created using `zkcli-app`:

```bash
npm --save-dev install mina-jest-matchers #TODO
```
Expand Down Expand Up @@ -65,7 +74,7 @@ export class Add extends SmartContract {
```
```bash
npx mina-testing-utils
npx mina-testing-utils repl


███╗ ███╗ ██╗ ███╗ ██╗ █████╗ ████████╗ ███████╗ ███████╗ ████████╗ ██╗ ███╗ ██╗ ██████╗ ██╗ ██╗ ████████╗ ██╗ ██╗ ███████╗
Expand All @@ -83,13 +92,10 @@ Please load the Mina REPL context by executing .loadMina before running any comm



mina-testing-utils> .load
load loadMina

mina-testing-utils> .loadMina
Snarky loaded successfully! You can access it through the mina object.

mina-testing-utils> let { Add } = await import("/Users/alan.verbner/Projects/globant/mina/04-zkapp-browser-ui/contracts/build/src/Add.js")
mina-testing-utils> let { Add } = await import("/Projects/yourProject/build/src/Add.js")

mina-testing-utils> let { priv: zkAppPrivateKey, pub: zkAppAddress } = mina.genKeyPair();

Expand All @@ -113,6 +119,20 @@ See it in action:
[![asciicast](https://asciinema.org/a/603288.svg)](https://asciinema.org/a/603288)
## Using the contract sizer
This tool also allows you to keep track of the amount of gates the contracts you create are creating thus giving an idea of the complexity it will involve using them. To try it, run:
```bash
npx mina-testing-utils circuits-sizer build/src/test-contract.js
```
Bear in mind you need to compile them first (if `zkapp-cli` is being used, it should be `npm run build`).
See it in action:
[![asciicast](https://asciinema.org/a/13DtDxa6nId5AtEhDcvZ3IyDf.svg)](https://asciinema.org/a/13DtDxa6nId5AtEhDcvZ3IyDf)
# Development
## Requirements
Expand Down
113 changes: 113 additions & 0 deletions cli/circuits-sizer.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const fs = require("fs");
const path = require("path");
const Table = require("cli-table");
const Spinnies = require("spinnies");

const spinnies = new Spinnies({ spinner: { frames: ["⚙️"] } });

/**
* Checks if the provided path is valid.
* @param {string} inputPath - The path to check.
* @returns {boolean} - Returns true if the path is valid, false otherwise.
*/
function isValidPath(inputPath) {
if (!fs.existsSync(inputPath)) {
console.error(`${inputPath} does not exist.`);
return false;
}

const stats = fs.statSync(inputPath);
if (!stats.isFile()) {
console.error(`${inputPath} is not a file.`);
return false;
}
return true;
}

/**
* Processes a contract.
* @param {string} contractName - The name of the contract.
* @param {object} contract - The contract object.
* @returns {Array} - Returns an array of methods.
*/
async function tryProcessContract(contractName, contract) {
try {
spinnies.add(contractName, {
text: `Compiling ${contractName}`,
});
await contract.compile();
spinnies.succeed(contractName, { text: `Compiling ${contractName}` });
const methods = [];
Object.keys(contract._methodMetadata).map((key) => {
methods.push([
contractName,
key,
contract._methodMetadata[key].gates.length,
]);
});
return methods;
} catch (err) {
spinnies.fail(conractName, {
text: `Error while compiling ${contractName}`,
});
}
}

/**
* Processes contracts from a file.
* @param {string} inputPath - The path to the file.
* @returns {Array} - Returns an array of rows.
*/
async function tryProcessContractsFromFile(inputPath) {
const rows = [];
try {
const contracts = await import(inputPath);

if (contracts.length == 0)
console.error(`No contract founds in ${inputPath}`);

return Promise.all(
Object.keys(contracts).map((contractName) => {
const contract = contracts[contractName];
if (!contract.compile) return [];
return tryProcessContract(contractName, contract);
})
);
} catch (err) {
console.log(err);
console.error(
`Couldn't import ${inputPath}, please check if it's a valid compiled contract.`
);
}

return rows;
}

/**
* Main function to process files.
* @param {Array} files - The array of file paths.
*/
module.exports = async (files) => {
const rows = (
await Promise.all(
files.map(async (inputPath) => {
if (!path.isAbsolute(inputPath)) {
inputPath = path.resolve(inputPath);
}
if (!isValidPath(inputPath)) return [];
return await tryProcessContractsFromFile(inputPath);
})
)
).flat(2);

if (rows.length == 0) {
console.warn("No contracts were found");
} else {
const table = new Table({
head: ["Contract", "Method", "Gates"],
rows: rows.sort((a, b) => b[2] - a[2]),
});

console.log(table.toString());
}
};
49 changes: 21 additions & 28 deletions cli/cli.cjs
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
#!/usr/bin/env node --experimental-vm-modules --experimental-wasm-threads

const chalk = require("chalk");
const { Command } = require("commander");
const { name, description, version } = require("../package.json");
const repl = require("./repl.cjs");
const { printBanner } = require("./gui.cjs");
const configureRepl = require("./repl.cjs");
const circuitsSizer = require("./circuits-sizer.cjs");

printBanner();
const program = new Command();

/**
* Generates a key pair using snarkyjs.
* @param {Object} snarkyjs - The snarkyjs library.
* @returns {Object} - An object containing the private and public keys.
*/
function genKeyPair(snarkyjs) {
return () => {
const priv = snarkyjs.PrivateKey.random();
const pub = priv.toPublicKey();
program.name(name).description(description).version(version);

return {
priv,
pub,
};
};
}
program
.command("repl")
.description("Runs Mina Testing Utils repl")
.action((str, options) => {
printBanner();
repl();
});

/**
* Dynamically imports a module from an absolute path.
* @param {string} absolutePath - The absolute path of the module to import.
* @returns {Promise} - A promise that resolves to the imported module.
*/
function dynamicImport(absolutePath) {
return import(absolutePath);
}
program
.command("circuits-sizer")
.description("Analyzes contracts gates amount")
.argument("<files...>", "Specify the file")
.action((files) => {
return circuitsSizer(files);
});

// Start a REPL session with the given options.
configureRepl(dynamicImport, genKeyPair);
program.parse();
41 changes: 34 additions & 7 deletions cli/repl.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const repl = require("repl");
const ora = require("ora");
const Spinnies = require("spinnies");
const homedir = require("os").homedir();
const spinnies = new Spinnies();

const { printLoadMinaErrorMsg, minaLoadedOkMsg } = require("./gui.cjs");

Expand Down Expand Up @@ -50,10 +51,9 @@ function configureRepl(dynamicImport, genKeyPair) {
REPL.defineCommand("loadMina", {
help: "Loads snarkyjs",
action() {
const spinner = ora({
spinnies.add("load", {
text: "Loading snarkyjs...",
discardStdin: false,
}).start();
});
this.clearBufferedCommand();
dynamicImport("snarkyjs")
.then((snarkyjs) => {
Expand All @@ -71,18 +71,45 @@ function configureRepl(dynamicImport, genKeyPair) {

Object.assign(this.context.mina, minaContext);

spinner.succeed(minaLoadedOkMsg());
spinnies.succeed("load", minaLoadedOkMsg());
console.log();
// Restore eval function.
this.displayPrompt();
REPL.eval = evalFn;
})
.catch((err) => {
console.log(err);
spinner.fail("Error while loading snarkyjs");
spinnies.fail("load", "Error while loading snarkyjs");
});
},
});
}

module.exports = configureRepl;
/**
* Generates a key pair using snarkyjs.
* @param {Object} snarkyjs - The snarkyjs library.
* @returns {Object} - An object containing the private and public keys.
*/
function genKeyPair(snarkyjs) {
return () => {
const priv = snarkyjs.PrivateKey.random();
const pub = priv.toPublicKey();

return {
priv,
pub,
};
};
}

/**
* Dynamically imports a module from an absolute path.
* @param {string} absolutePath - The absolute path of the module to import.
* @returns {Promise} - A promise that resolves to the imported module.
*/
function dynamicImport(absolutePath) {
return import(absolutePath);
}

// Start a REPL session with the given options.
module.exports = () => configureRepl(dynamicImport, genKeyPair);
Loading

0 comments on commit fbd3837

Please sign in to comment.