From 1b88adcb3c61cd832b6f45b46d5f99692599dbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Mon, 16 Dec 2024 18:37:46 +0000 Subject: [PATCH] chore(docs): updating noirjs tutorial for 1.0.0 (#6792) Co-authored-by: josh crites --- cspell.json | 3 + docs/docs/tutorials/noirjs_app.md | 380 ++++++++---------- .../img/tutorials/noirjs_webapp/webapp1.png | Bin 0 -> 15009 bytes 3 files changed, 163 insertions(+), 220 deletions(-) create mode 100644 docs/static/img/tutorials/noirjs_webapp/webapp1.png diff --git a/cspell.json b/cspell.json index 5c707e92e21..9a4bca1e339 100644 --- a/cspell.json +++ b/cspell.json @@ -32,6 +32,7 @@ "boilerplates", "bridgekeeper", "brillig", + "bunx", "bytecount", "cachix", "callsite", @@ -242,9 +243,11 @@ "walkdir", "wasi", "wasmer", + "webapps", "Weierstraß", "whitespace", "whitespaces", + "YOLO", "zkhash", "zshell" ], diff --git a/docs/docs/tutorials/noirjs_app.md b/docs/docs/tutorials/noirjs_app.md index 6e69ea0bbed..8967ee005ce 100644 --- a/docs/docs/tutorials/noirjs_app.md +++ b/docs/docs/tutorials/noirjs_app.md @@ -1,186 +1,91 @@ --- -title: Building a web app with NoirJS +title: Building a web app with Noir and Barretenberg description: Learn how to setup a new app that uses Noir to generate and verify zero-knowledge SNARK proofs in a typescript or javascript environment. keywords: [how to, guide, javascript, typescript, noir, barretenberg, zero-knowledge, proofs, app] sidebar_position: 0 pagination_next: noir/concepts/data_types/index --- -NoirJS is a set of packages meant to work both in a browser and a server environment. In this tutorial, we will build a simple web app using them. From here, you should get an idea on how to proceed with your own Noir projects! +NoirJS is a Typescript package meant to work both in a browser and a server environment. -You can find the complete app code for this guide [here](https://github.com/noir-lang/tiny-noirjs-app). - -## Setup - -:::note - -Feel free to use whatever versions, just keep in mind that Nargo and the NoirJS packages are meant to be in sync. For example, Nargo 0.31.x matches `noir_js@0.31.x`, etc. - -In this guide, we will be pinned to 0.31.0. - -::: - -Before we start, we want to make sure we have Node, Nargo and the Barretenberg proving system (`bb`) installed. - -We start by opening a terminal and executing `node --version`. If we don't get an output like `v20.10.0`, that means node is not installed. Let's do that by following the handy [nvm guide](https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script). - -As for `Nargo`, we can follow the [Nargo guide](../getting_started/quick_start.md) to install it. If you're lazy, just paste this on a terminal and run `noirup`: - -```sh -curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash -``` +In this tutorial, we will combine NoirJS with Aztec's Barretenberg backend to build a simple web app. From here, you should get an idea on how to proceed with your own Noir projects! -Follow the instructions on [this page](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg/cpp/src/barretenberg/bb#installation) to install `bb`. -Version 0.41.0 is compatible with `nargo` version 0.31.0, which you can install with `bbup -v 0.41.0` once `bbup` is installed. - -Easy enough. Onwards! - -## Our project - -ZK is a powerful technology. An app that doesn't reveal one of the inputs to _anyone_ is almost unbelievable, yet Noir makes it as easy as a single line of code. - -In fact, it's so simple that it comes nicely packaged in `nargo`. Let's do that! +You can find the complete app code for this guide [here](https://github.com/noir-lang/tiny-noirjs-app). -### Nargo +## Dependencies -Run: +Before we start, we want to make sure we have Node installed. For convenience (and speed), we can just install [Bun](https://bun.sh) as our package manager, and Node will work out-of-the-box: ```bash -nargo new circuit +curl -fsSL https://bun.sh/install | bash ``` -And... That's about it. Your program is ready to be compiled and run. +Let's go barebones. Doing the bare minimum is not only simple, but also allows you to easily adapt it to almost any frontend framework. -To compile, let's `cd` into the `circuit` folder to enter our project, and call: +Barebones means we can immediately start with the dependencies even on an empty folder 😈: ```bash -nargo compile +bun i @noir-lang/noir_wasm@1.0.0-beta.0 @noir-lang/noir_js@1.0.0-beta.0 @aztec/bb.js@0.63.1 ``` -This compiles our circuit into `json` format and add it to a new `target` folder. +Wait, what are these dependencies? -:::info +- `noir_wasm` is the `wasm` version of the Noir compiler. Although most developers prefer to use `nargo` for compiling, there's nothing wrong with `noir_wasm`. We like `noir_wasm`. +- `noir_js` is the main Noir package. It will execute our program, and generate the witness that will be sent to the backend. +- `bb.js` is the Typescript interface for Aztec's Barretenberg proving backend. It also uses the `wasm` version in order to run on the browser. -At this point in the tutorial, your folder structure should look like this: +:::info -```tree -. -└── circuit <---- our working directory - β”œβ”€β”€ Nargo.toml - β”œβ”€β”€ src - β”‚ └── main.nr - └── target - └── circuit.json -``` +In this guide, we will install versions pinned to 1.0.0-beta.0. These work with Barretenberg version 0.63.1, so we are using that one version too. Feel free to try with older or later versions, though! ::: -### Node and Vite - -If you want to explore Nargo, feel free to go on a side-quest now and follow the steps in the -[getting started](../getting_started/quick_start.md) guide. However, we want our app to run on the browser, so we need Vite. +## Setting up our Noir program -Vite is a powerful tool to generate static websites. While it provides all kinds of features, let's just go barebones with some good old vanilla JS. +ZK is a powerful technology. An app that reveals computational correctness but doesn't reveal some of its inputs is almost unbelievable, yet Noir makes it as easy as a single line of code. -To do this this, go back to the previous folder (`cd ..`) and create a new vite project by running `npm create vite` and choosing "Vanilla" and "Javascript". +:::tip -A wild `vite-project` directory should now appear in your root folder! Let's not waste any time and dive right in: +It's not just you. We also enjoy syntax highlighting. [Check out the Language Server](../tooling/language_server.md) -```bash -cd vite-project -``` +::: -### Setting Up Vite and Configuring the Project - -Before we proceed with any coding, let's get our environment tailored for Noir. We'll start by laying down the foundations with a `vite.config.js` file. This little piece of configuration is our secret sauce for making sure everything meshes well with the NoirJS libraries and other special setups we might need, like handling WebAssembly modules. Here’s how you get that going: - -#### Creating the vite.config.js - -In your freshly minted `vite-project` folder, create a new file named `vite.config.js` and open it in your code editor. Paste the following to set the stage: - -```javascript -import { defineConfig } from 'vite'; -import copy from 'rollup-plugin-copy'; -import fs from 'fs'; -import path from 'path'; - -const wasmContentTypePlugin = { - name: 'wasm-content-type-plugin', - configureServer(server) { - server.middlewares.use(async (req, res, next) => { - if (req.url.endsWith('.wasm')) { - res.setHeader('Content-Type', 'application/wasm'); - const newPath = req.url.replace('deps', 'dist'); - const targetPath = path.join(__dirname, newPath); - const wasmContent = fs.readFileSync(targetPath); - return res.end(wasmContent); - } - next(); - }); - }, -}; +All you need is a `main.nr` and a `Nargo.toml` file. You can follow the [noirup](../getting_started/noir_installation.md) installation and just run `noirup -v 1.0.0-beta.0`, or just create them by hand: -export default defineConfig(({ command }) => { - if (command === 'serve') { - return { - build: { - target: 'esnext', - rollupOptions: { - external: ['@aztec/bb.js'] - } - }, - optimizeDeps: { - esbuildOptions: { - target: 'esnext' - } - }, - plugins: [ - copy({ - targets: [{ src: 'node_modules/**/*.wasm', dest: 'node_modules/.vite/dist' }], - copySync: true, - hook: 'buildStart', - }), - command === 'serve' ? wasmContentTypePlugin : [], - ], - }; - } - - return {}; -}); +```bash +mkdir -p circuit/src +touch circuit/src/main.nr circuit/Nargo.toml ``` -#### Install Dependencies - -Now that our stage is set, install the necessary NoirJS packages along with our other dependencies: +To make our program interesting, let's give it a real use-case scenario: Bob wants to prove he is older than 18, without disclosing his age. Open `main.nr` and write: -```bash -npm install && npm install @noir-lang/backend_barretenberg@0.31.0 @noir-lang/noir_js@0.31.0 -npm install rollup-plugin-copy --save-dev +```rust +fn main(age: u8) { + assert(age >= 18); +} ``` -:::info - -At this point in the tutorial, your folder structure should look like this: +This program accepts a private input called age, and simply proves this number is higher than 18. But to run this code, we need to give the compiler a `Nargo.toml` with at least a name and a type: -```tree -. -└── circuit - └── ...etc... -└── vite-project <---- our working directory - └── ...etc... +```toml +[package] +name = "circuit" +type = "bin" ``` -::: +This is all that we need to get started with Noir. -#### Some cleanup +![my heart is ready for you, noir.js](@site/static/img/memes/titanic.jpeg) -`npx create vite` is amazing but it creates a bunch of files we don't really need for our simple example. Actually, let's just delete everything except for `vite.config.js`, `index.html`, `main.js` and `package.json`. I feel lighter already. +## Setting up our app -![my heart is ready for you, noir.js](@site/static/img/memes/titanic.jpeg) +Remember when apps only had one `html` and one `js` file? Well, that's enough for Noir webapps. Let's create them: -## HTML +```bash +touch index.html index.js +``` -Our app won't run like this, of course. We need some working HTML, at least. Let's open our broken-hearted `index.html` and replace everything with this code snippet: +And add something useful to our HTML file: ```html @@ -200,11 +105,11 @@ Our app won't run like this, of course. We need some working HTML, at least. Let - +

Noir app

- - + +

Logs

@@ -214,32 +119,26 @@ Our app won't run like this, of course. We need some working HTML, at least. Let ``` -It _could_ be a beautiful UI... Depending on which universe you live in. - -## Some good old vanilla Javascript - -Our love for Noir needs undivided attention, so let's just open `main.js` and delete everything (this is where the romantic scenery becomes a bit creepy). +It _could_ be a beautiful UI... Depending on which universe you live in. In any case, we're using some scary CSS to make two boxes that will show cool things on the screen. -Start by pasting in this boilerplate code: +As for the JS, real madmen could just `console.log` everything, but let's say we want to see things happening (the true initial purpose of JS... right?). Here's some boilerplate for that. Just paste it in `index.js`: ```js -function display(container, msg) { - const c = document.getElementById(container); - const p = document.createElement('p'); - p.textContent = msg; - c.appendChild(p); -} +const show = (id, content) => { + const container = document.getElementById(id); + container.appendChild(document.createTextNode(content)); + container.appendChild(document.createElement("br")); +}; -document.getElementById('submitGuess').addEventListener('click', async () => { - try { - // here's where love happens - } catch (err) { - display('logs', 'Oh πŸ’” Wrong guess'); - } +document.getElementById("submit").addEventListener("click", async () => { + try { + // noir goes here + } catch { + show("logs", "Oh πŸ’”"); + } }); -``` -The display function doesn't do much. We're simply manipulating our website to see stuff happening. For example, if the proof fails, it will simply log a broken heart 😒 +``` :::info @@ -248,30 +147,56 @@ At this point in the tutorial, your folder structure should look like this: ```tree . └── circuit - └── ...same as above -└── vite-project - β”œβ”€β”€ vite.config.js - β”œβ”€β”€ main.js - β”œβ”€β”€ package.json - └── index.html + └── src + └── main.nr + Nargo.toml + index.js + package.json + index.html + ...etc ``` -You'll see other files and folders showing up (like `package-lock.json`, `node_modules`) but you shouldn't have to care about those. - ::: -## Some NoirJS +## Compile compile compile -We're starting with the good stuff now. If you've compiled the circuit as described above, you should have a `json` file we want to import at the very top of our `main.js` file: +Finally we're up for something cool. But before we can execute a Noir program, we need to compile it into ACIR: an abstract representation. Here's where `noir_wasm` comes in. -```ts -import circuit from '../circuit/target/circuit.json'; +`noir_wasm` expects a filesystem so it can resolve dependencies. While we could use the `public` folder, let's just import those using the nice `?url` syntax provided by vite. At the top of the file: + +```js +import { compile, createFileManager } from "@noir-lang/noir_wasm" + +import main from "./circuit/src/main.nr?url"; +import nargoToml from "./circuit/Nargo.toml?url"; ``` -[Noir is backend-agnostic](../index.mdx#whats-new-about-noir). We write Noir, but we also need a proving backend. That's why we need to import and instantiate the two dependencies we installed above: `BarretenbergBackend` and `Noir`. Let's import them right below: +Compiling on the browser is common enough that `createFileManager` already gives us a nice in-memory filesystem we can use. So all we need to compile is fetching these files, writing them to our filesystem, and compile. Add this function: ```js -import { BarretenbergBackend, BarretenbergVerifier as Verifier } from '@noir-lang/backend_barretenberg'; +export async function getCircuit() { + const fm = createFileManager("/"); + const { body } = await fetch(main); + const { body: nargoTomlBody } = await fetch(nargoToml); + + fm.writeFile("./src/main.nr", body); + fm.writeFile("./Nargo.toml", nargoTomlBody); + return await compile(fm); +} +``` + +:::tip + +As you can imagine, with `node` it's all conveniently easier since you get native access to `fs`... + +::: + +## Some more JS + +We're starting with the good stuff now. We want to execute our circuit to get the witness, and then feed that witness to Barretenberg. Luckily, both packages are quite easy to work with. Let's import them at the top of the file: + +```js +import { UltraHonkBackend } from '@aztec/bb.js'; import { Noir } from '@noir-lang/noir_js'; ``` @@ -279,88 +204,103 @@ And instantiate them inside our try-catch block: ```ts // try { -const backend = new BarretenbergBackend(circuit); -const noir = new Noir(circuit); +const { program } = await getCircuit(); +const noir = new Noir(program); +const backend = new UltraHonkBackend(program.bytecode); // } ``` -:::note +:::warning -For the remainder of the tutorial, everything will be happening inside the `try` block +WASMs are not always easy to work with. In our case, `vite` likes serving them with the wrong MIME type. There are different fixes but we found the easiest one is just YOLO instantiating the WASMs manually. Paste this at the top of the file, just below the other imports, and it will work just fine: + +```js +import initNoirC from "@noir-lang/noirc_abi"; +import initACVM from "@noir-lang/acvm_js"; +import acvm from "@noir-lang/acvm_js/web/acvm_js_bg.wasm?url"; +import noirc from "@noir-lang/noirc_abi/web/noirc_abi_wasm_bg.wasm?url"; +await Promise.all([initACVM(fetch(acvm)), initNoirC(fetch(noirc))]); +``` ::: -## Our app +## Executing and proving -Now for the app itself. We're capturing whatever is in the input when people press the submit button. Just add this: +Now for the app itself. We're capturing whatever is in the input when people press the submit button. Inside our `try` block, let's just grab that input and get its value. Noir will gladly execute it, and give us a witness: ```js -const x = parseInt(document.getElementById('guessInput').value); -const input = { x, y: 2 }; +const age = document.getElementById("age").value; +show("logs", "Generating witness... ⏳"); +const { witness } = await noir.execute({ age }); +show("logs", "Generated witness... βœ…"); + ``` +:::note + +For the remainder of the tutorial, everything will be happening inside the `try` block + +::: + Now we're ready to prove stuff! Let's feed some inputs to our circuit and calculate the proof: ```js -await setup(); // let's squeeze our wasm inits here - -display('logs', 'Generating proof... βŒ›'); -const { witness } = await noir.execute(input); +show("logs", "Generating proof... ⏳"); const proof = await backend.generateProof(witness); -display('logs', 'Generating proof... βœ…'); -display('results', proof.proof); +show("logs", "Generated proof... βœ…"); +show("results", proof.proof); ``` -You're probably eager to see stuff happening, so go and run your app now! +Our program is technically **done** . You're probably eager to see stuff happening! To serve this in a convenient way, we can use a bundler like `vite` by creating a `vite.config.js` file: + +```bash +touch vite.config.js +``` -From your terminal, run `npm run dev`. If it doesn't open a browser for you, just visit `localhost:5173`. You should now see the worst UI ever, with an ugly input. +`vite` helps us with a little catch: `bb.js` in particular uses top-level awaits which aren't supported everywhere. So we can add this to the `vite.config.js` to make the bundler optimize them: -![Getting Started 0](@site/static/img/noir_getting_started_1.png) +```js +export default { optimizeDeps: { esbuildOptions: { target: "esnext" } } }; +``` + +This should be enough for vite. We don't even need to install it, just run: + +```bash +bunx vite +``` -Now, our circuit says `fn main(x: Field, y: pub Field)`. This means only the `y` value is public, and it's hardcoded above: `input = { x, y: 2 }`. In other words, you won't need to send your secret`x` to the verifier! +If it doesn't open a browser for you, just visit `localhost:5173`. You should now see the worst UI ever, with an ugly input. -By inputting any number other than 2 in the input box and clicking "submit", you should get a valid proof. Otherwise the proof won't even generate correctly. By the way, if you're human, you shouldn't be able to understand anything on the "proof" box. That's OK. We like you, human ❀️. +![Noir Webapp UI](@site/static/img/tutorials/noirjs_webapp/webapp1.png) + +Now, our circuit requires a private input `fn main(age: u8)`, and fails if it is less than 18. Let's see if it works. Submit any number above 18 (as long as it fits in 8 bits) and you should get a valid proof. Otherwise the proof won't even generate correctly. + +By the way, if you're human, you shouldn't be able to understand anything on the "proof" box. That's OK. We like you, human ❀️. ## Verifying Time to celebrate, yes! But we shouldn't trust machines so blindly. Let's add these lines to see our proof being verified: ```js -display('logs', 'Verifying proof... βŒ›'); +show('logs', 'Verifying proof... βŒ›'); const isValid = await backend.verifyProof(proof); - -// or to cache and use the verification key: -// const verificationKey = await backend.getVerificationKey(); -// const verifier = new Verifier(); -// const isValid = await verifier.verifyProof(proof, verificationKey); - -if (isValid) display('logs', 'Verifying proof... βœ…'); +show("logs", `Proof is ${isValid ? "valid" : "invalid"}... βœ…`); ``` You have successfully generated a client-side Noir web app! ![coded app without math knowledge](@site/static/img/memes/flextape.jpeg) -## Further Reading - -You can see how noirjs is used in a full stack Next.js hardhat application in the [noir-starter repo here](https://github.com/noir-lang/noir-starter/tree/main/vite-hardhat). The example shows how to calculate a proof in the browser and verify it with a deployed Solidity verifier contract from noirjs. - -You should also check out the more advanced examples in the [noir-examples repo](https://github.com/noir-lang/noir-examples), where you'll find reference usage for some cool apps. +## Next steps -## UltraHonk Backend +At this point, you have a working ZK app that works on the browser. Actually, it works on a mobile phone too! -Barretenberg has recently exposed a new UltraHonk backend. We can use UltraHonk in NoirJS after version 0.33.0. Everything will be the same as the tutorial above, except that the class we need to import will change: +If you want to continue learning by doing, here are some challenges for you: -```js -import { UltraHonkBackend, UltraHonkVerifier as Verifier } from '@noir-lang/backend_barretenberg'; -``` - -The backend will then be instantiated as such: - -```js -const backend = new UltraHonkBackend(circuit); -``` +- Install [nargo](https://noir-lang.org/docs/getting_started/noir_installation) and write [Noir tests](../tooling/testing) +- Change the circuit to accept a [public input](../noir/concepts/data_types/#private--public-types) as the cutoff age. It could be different depending on the purpose, for example! +- Enjoy Noir's Rust-like syntax and write a struct `Country` that implements a trait `MinAge` with a method `get_min_age`. Then, make a struct `Person` have an `u8` as its age and a country of type `Country`. You can pass a `person` in JS just like a JSON object `person: { age, country: { min_age: 18 }}` -Then all the commands to prove and verify your circuit will be same. +The world is your stage, just have fun with ZK! You can see how noirjs is used in a full stack Next.js hardhat application in the [noir-starter repo here](https://github.com/noir-lang/noir-starter/tree/main/vite-hardhat). The example shows how to calculate a proof in the browser and verify it with a deployed Solidity verifier contract from noirjs. -The only feature currently unsupported with UltraHonk are [recursive proofs](../explainers/explainer-recursion.md). +Check out other starters, tools, or just cool projects in the [awesome noir repository](https://github.com/noir-lang/awesome-noir). diff --git a/docs/static/img/tutorials/noirjs_webapp/webapp1.png b/docs/static/img/tutorials/noirjs_webapp/webapp1.png new file mode 100644 index 0000000000000000000000000000000000000000..7591cd827e3ef91b0dde36e492f7636fb3744640 GIT binary patch literal 15009 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