Skip to content

Commit

Permalink
chore(docs): documenting reference, how-to and explainer on the usage…
Browse files Browse the repository at this point in the history
… of oracles (#4002)

# Description

This PR adds some more in-depth documentation on oracles, what they are
and how to use them.

## Problem\*

Closes #3971

Oracles can now be resolved with JSON RPC calls, which is a powerful
feature for developers. This PR attempts to provide some information on
how they can be part of a development flow

## Summary\*

- [ ] Adds the nargo commands for oracles
- [ ] Adds an explainer on what are oracles and what can you do with
them
- [ ] Adds a how-to guide on using oracles
- [ ] Adds a tutorial on a small project using oracles

---------

Co-authored-by: Savio <[email protected]>
Co-authored-by: josh crites <[email protected]>
  • Loading branch information
3 people authored Jan 16, 2024
1 parent 916fd15 commit d6a16d0
Show file tree
Hide file tree
Showing 25 changed files with 745 additions and 18 deletions.
4 changes: 3 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
"wasi",
"wasmer",
"Weierstraß",
"zshell"
"zshell",
"nouner",
"devcontainer"
],
"ignorePaths": [
"./**/node_modules/**",
Expand Down
57 changes: 57 additions & 0 deletions docs/docs/explainers/explainer-oracle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
title: Oracles
description: This guide provides an in-depth understanding of how Oracles work in Noir programming. Learn how to use outside calculations in your programs, constrain oracles, and understand their uses and limitations.
keywords:
- Noir Programming
- Oracles
- JSON-RPC
- Foreign Call Handlers
- Constrained Functions
- Blockchain Programming
sidebar_position: 1
---

If you've seen "The Matrix" you may recall "The Oracle" as Gloria Foster smoking cigarettes and baking cookies. While she appears to "know things", she is actually providing a calculation of a pre-determined future. Noir Oracles are similar, in a way. They don't calculate the future (yet), but they allow you to use outside calculations in your programs.

![matrix oracle prediction](@site/static/img/memes/matrix_oracle.jpeg)

A Noir program is usually self-contained. You can pass certain inputs to it, and it will generate a deterministic output for those inputs. But what if you wanted to defer some calculation to an outside process or source?

Oracles are functions that provide this feature.

## Use cases

An example usage for Oracles is proving something on-chain. For example, proving that the ETH-USDC quote was below a certain target at a certain block time. Or even making more complex proofs like proving the ownership of an NFT as an anonymous login method.

Another interesting use case is to defer expensive calculations to be made outside of the Noir program, and then constraining the result; similar to the use of [unconstrained functions](../noir/concepts//unconstrained.md).

In short, anything that can be constrained in a Noir program but needs to be fetched from an external source is a great candidate to be used in oracles.

## Constraining oracles

Just like in The Matrix, Oracles are powerful. But with great power, comes great responsibility. Just because you're using them in a Noir program doesn't mean they're true. Noir has no superpowers. If you want to prove that Portugal won the Euro Cup 2016, you're still relying on potentially untrusted information.

To give a concrete example, Alice wants to login to the [NounsDAO](https://nouns.wtf/) forum with her username "noir_nouner" by proving she owns a noun without revealing her ethereum address. Her Noir program could have a oracle call like this:

```rust
#[oracle(getNoun)]
unconstrained fn get_noun(address: Field) -> Field
```

This oracle could naively resolve with the number of Nouns she possesses. However, it is useless as a trusted source, as the oracle could resolve to anything Alice wants. In order to make this oracle call actually useful, Alice would need to constrain the response from the oracle, by proving her address and the noun count belongs to the state tree of the contract.

In short, **Oracles don't prove anything. Your Noir program does.**

:::danger

If you don't constrain the return of your oracle, you could be clearly opening an attack vector on your Noir program. Make double-triple sure that the return of an oracle call is constrained!

:::

## How to use Oracles

On CLI, Nargo resolves oracles by making JSON RPC calls, which means it would require an RPC node to be running.

In JavaScript, NoirJS accepts and resolves arbitrary call handlers (that is, not limited to JSON) as long as they matches the expected types the developer defines. Refer to [Foreign Call Handler](../reference/NoirJS/noir_js/type-aliases/ForeignCallHandler.md) to learn more about NoirJS's call handling.

If you want to build using oracles, follow through to the [oracle guide](../how_to/how-to-oracles.md) for a simple example on how to do that.
280 changes: 280 additions & 0 deletions docs/docs/how_to/how-to-oracles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
---
title: How to use Oracles
description: Learn how to use oracles in your Noir program with examples in both Nargo and NoirJS. This guide also covers writing a JSON RPC server and providing custom foreign call handlers for NoirJS.
keywords:
- Noir Programming
- Oracles
- Nargo
- NoirJS
- JSON RPC Server
- Foreign Call Handlers
sidebar_position: 1
---

This guide shows you how to use oracles in your Noir program. For the sake of clarity, it assumes that:

- You have read the [explainer on Oracles](../explainers/explainer-oracle.md) and are comfortable with the concept.
- You have a Noir program to add oracles to. You can create one using the [vite-hardhat starter](https://github.com/noir-lang/noir-starter/tree/main/vite-hardhat) as a boilerplate.
- You understand the concept of a JSON-RPC server. Visit the [JSON-RPC website](https://www.jsonrpc.org/) if you need a refresher.
- You are comfortable with server-side JavaScript (e.g. Node.js, managing packages, etc.).

For reference, you can find the snippets used in this tutorial on the [Aztec DevRel Repository](https://github.com/AztecProtocol/dev-rel/tree/main/how_to_oracles/code-snippets/how-to-oracles).

## Rundown

This guide has 3 major steps:

1. How to modify our Noir program to make use of oracle calls as unconstrained functions
2. How to write a JSON RPC Server to resolve these oracle calls with Nargo
3. How to use them in Nargo and how to provide a custom resolver in NoirJS

## Step 1 - Modify your Noir program

An oracle is defined in a Noir program by defining two methods:

- An unconstrained method - This tells the compiler that it is executing an [unconstrained functions](../noir/concepts//unconstrained.md).
- A decorated oracle method - This tells the compiler that this method is an RPC call.

An example of an oracle that returns a `Field` would be:

```rust
#[oracle(getSqrt)]
unconstrained fn sqrt(number: Field) -> Field { }

unconstrained fn get_sqrt(number: Field) -> Field {
sqrt(number)
}
```

In this example, we're wrapping our oracle function in a unconstrained method, and decorating it with `oracle(getSqrt)`. We can then call the unconstrained function as we would call any other function:

```rust
fn main(input: Field) {
let sqrt = get_sqrt(input);
}
```

In the next section, we will make this `getSqrt` (defined on the `sqrt` decorator) be a method of the RPC server Noir will use.

:::danger

As explained in the [Oracle Explainer](../explainers/explainer-oracle.md), this `main` function is unsafe unless you constrain its return value. For example:

```rust
fn main(input: Field) {
let sqrt = get_sqrt(input);
assert(sqrt.pow_32(2) as u64 == input as u64); // <---- constrain the return of an oracle!
}
```

:::

:::info

Currently, oracles only work with single params or array params. For example:

```rust
#[oracle(getSqrt)]
unconstrained fn sqrt([Field; 2]) -> [Field; 2] { }
```

:::

## Step 2 - Write an RPC server

Brillig will call *one* RPC server. Most likely you will have to write your own, and you can do it in whatever language you prefer. In this guide, we will do it in Javascript.

Let's use the above example of an oracle that consumes an array with two `Field` and returns their square roots:

```rust
#[oracle(getSqrt)]
unconstrained fn sqrt(input: [Field; 2]) -> [Field; 2] { }

unconstrained fn get_sqrt(input: [Field; 2]) -> [Field; 2] {
sqrt(input)
}

fn main(input: [Field; 2]) {
let sqrt = get_sqrt(input);
assert(sqrt[0].pow_32(2) as u64 == input[0] as u64);
assert(sqrt[1].pow_32(2) as u64 == input[1] as u64);
}
```

:::info

Why square root?

In general, computing square roots is computationally more expensive than multiplications, which takes a toll when speaking about ZK applications. In this case, instead of calculating the square root in Noir, we are using our oracle to offload that computation to be made in plain. In our circuit we can simply multiply the two values.

:::

Now, we should write the correspondent RPC server, starting with the [default JSON-RPC 2.0 boilerplate](https://www.npmjs.com/package/json-rpc-2.0#example):

```js
import { JSONRPCServer } from "json-rpc-2.0";
import express from "express";
import bodyParser from "body-parser";

const app = express();
app.use(bodyParser.json());

const server = new JSONRPCServer();
app.post("/", (req, res) => {
const jsonRPCRequest = req.body;
server.receive(jsonRPCRequest).then((jsonRPCResponse) => {
if (jsonRPCResponse) {
res.json(jsonRPCResponse);
} else {
res.sendStatus(204);
}
});
});

app.listen(5555);
```

Now, we will add our `getSqrt` method, as expected by the `#[oracle(getSqrt)]` decorator in our Noir code. It maps through the params array and returns their square roots:

```js
server.addMethod("getSqrt", async (params) => {
const values = params[0].Array.map(({ inner }) => {
return { inner: `${Math.sqrt(parseInt(inner, 16))}` };
});
return { values: [{ Array: values }] };
});
```

:::tip

Brillig expects an object with an array of values. Each value is an object declaring to be `Single` or `Array` and returning a `inner` property *as a string*. For example:

```json
{ "values": [{ "Array": [{ "inner": "1" }, { "inner": "2"}]}]}
{ "values": [{ "Single": { "inner": "1" }}]}
{ "values": [{ "Single": { "inner": "1" }}, { "Array": [{ "inner": "1", { "inner": "2" }}]}]}
```

If you're using Typescript, the following types may be helpful in understanding the expected return value and making sure they're easy to follow:

```js
interface Value {
inner: string,
}

interface SingleForeignCallParam {
Single: Value,
}

interface ArrayForeignCallParam {
Array: Value[],
}

type ForeignCallParam = SingleForeignCallParam | ArrayForeignCallParam;

interface ForeignCallResult {
values: ForeignCallParam[],
}
```

:::

## Step 3 - Usage with Nargo

Using the [`nargo` CLI tool](../getting_started/installation/index.md), you can use oracles in the `nargo test`, `nargo execute` and `nargo prove` commands by passing a value to `--oracle-resolver`. For example:

```bash
nargo test --oracle-resolver http://localhost:5555
```

This tells `nargo` to use your RPC Server URL whenever it finds an oracle decorator.

## Step 4 - Usage with NoirJS

In a JS environment, an RPC server is not strictly necessary, as you may want to resolve your oracles without needing any JSON call at all. NoirJS simply expects that you pass a callback function when you generate proofs, and that callback function can be anything.

For example, if your Noir program expects the host machine to provide CPU pseudo-randomness, you could simply pass it as the `foreignCallHandler`. You don't strictly need to create an RPC server to serve pseudo-randomness, as you may as well get it directly in your app:

```js
const foreignCallHandler = (name, inputs) => crypto.randomBytes(16) // etc

await noir.generateFinalProof(inputs, foreignCallHandler)
```

As one can see, in NoirJS, the [`foreignCallHandler`](../reference/NoirJS/noir_js/type-aliases/ForeignCallHandler.md) function simply means "a callback function that returns a value of type [`ForeignCallOutput`](../reference/NoirJS/noir_js/type-aliases/ForeignCallOutput.md). It doesn't have to be an RPC call like in the case for Nargo.

:::tip

Does this mean you don't have to write an RPC server like in [Step #2](#step-2---write-an-rpc-server)?

You don't technically have to, but then how would you run `nargo test` or `nargo prove`? To use both `Nargo` and `NoirJS` in your development flow, you will have to write a JSON RPC server.

:::

In this case, let's make `foreignCallHandler` call the JSON RPC Server we created in [Step #2](#step-2---write-an-rpc-server), by making it a JSON RPC Client.

For example, using the same `getSqrt` program in [Step #1](#step-1---modify-your-noir-program) (comments in the code):

```js
import { JSONRPCClient } from "json-rpc-2.0";

// declaring the JSONRPCClient
const client = new JSONRPCClient((jsonRPCRequest) => {
// hitting the same JSON RPC Server we coded above
return fetch("http://localhost:5555", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify(jsonRPCRequest),
}).then((response) => {
if (response.status === 200) {
return response
.json()
.then((jsonRPCResponse) => client.receive(jsonRPCResponse));
} else if (jsonRPCRequest.id !== undefined) {
return Promise.reject(new Error(response.statusText));
}
});
});

// declaring a function that takes the name of the foreign call (getSqrt) and the inputs
const foreignCallHandler = async (name, input) => {
// notice that the "inputs" parameter contains *all* the inputs
// in this case we to make the RPC request with the first parameter "numbers", which would be input[0]
const oracleReturn = await client.request(name, [
{ Array: input[0].map((i) => ({ inner: i.toString("hex") })) },
]);
return [oracleReturn.values[0].Array.map((x) => x.inner)];
};

// the rest of your NoirJS code
const input = { input: [4, 16] };
const { witness } = await noir.execute(numbers, foreignCallHandler);
```

:::tip

If you're in a NoirJS environment running your RPC server together with a frontend app, you'll probably hit a familiar problem in full-stack development: requests being blocked by [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policy. For development only, you can simply install and use the [`cors` npm package](https://www.npmjs.com/package/cors) to get around the problem:

```bash
yarn add cors
```

and use it as a middleware:

```js
import cors from "cors";

const app = express();
app.use(cors())
```

:::

## Conclusion

Hopefully by the end of this guide, you should be able to:

- Write your own logic around Oracles and how to write a JSON RPC server to make them work with your Nargo commands.
- Provide custom foreign call handlers for NoirJS.
2 changes: 1 addition & 1 deletion docs/docs/noir/concepts/comments.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description:
ignored by the compiler, but it can be read by programmers. Single-line and multi-line comments
are supported in Noir.
keywords: [Noir programming language, comments, single-line comments, multi-line comments]
sidebar_position: 9
sidebar_position: 10
---

A comment is a line in your codebase which the compiler ignores, however it can be read by
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/noir/concepts/data_bus.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Data Bus
sidebar_position: 12
sidebar_position: 13
---
**Disclaimer** this feature is experimental, do not use it!

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/noir/concepts/data_types/function_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ fn main() {
```

A function type also has an optional capture environment - this is necessary to support closures.
See [Lambdas](@site/docs/noir/concepts/lambdas.md) for more details.
See [Lambdas](../lambdas.md) for more details.
2 changes: 1 addition & 1 deletion docs/docs/noir/concepts/distinct.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Distinct Witnesses
sidebar_position: 10
sidebar_position: 11
---

The `distinct` keyword prevents repetitions of witness indices in the program's ABI. This ensures
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/noir/concepts/generics.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Generics
description: Learn how to use Generics in Noir
keywords: [Noir, Rust, generics, functions, structs]
sidebar_position: 6
sidebar_position: 7
---

Generics allow you to use the same functions with multiple different concrete data types. You can
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/noir/concepts/lambdas.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Lambdas
description: Learn how to use anonymous functions in Noir programming language.
keywords: [Noir programming language, lambda, closure, function, anonymous function]
sidebar_position: 8
sidebar_position: 9
---

## Introduction
Expand Down
Loading

0 comments on commit d6a16d0

Please sign in to comment.