-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(docs): documenting reference, how-to and explainer on the usage…
… 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
1 parent
916fd15
commit d6a16d0
Showing
25 changed files
with
745 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.