Skip to main content

Tutorial 7: Oracles

You can use an oracle when your smart contract needs to consume data from the outside world.

Learn about zkOracles in this 5-minute video:

Prerequisites

  • Make sure you have the zkApp CLI installed:

    $ npm install -g zkapp-cli
  • Ensure your environment meets the Prerequisites for zkApp Developer Tutorials.

This tutorial has been tested with:

High-Level Overview

In this tutorial, you are going to build an oracle that retrieves data from the REST API and you also are going to write the smart contract that consumes information from this oracle.

  1. Retrieve data from the REST API that provides mock credit score information for two users: one with a high credit score (user with id=1) and one with a low credit score (users with id > 1).
  2. The smart contract consumes this information and allows users to prove their credit score is above a certain threshold (for example, higher than 700).

Using the smart contract, users can generate an attestation that their credit score is above a certain value. To maintain their privacy, users can prove this fact to a third party without sharing the exact credit score or other personal information.

This tutorial uses a mock credit score API as the data source and provides a foundation to create an oracle for any type of data. Just alter the code to query data from whatever source you need, any other REST API, for example.

How Oracles Work

Oracles connect blockchain smart contracts with the outside world to get data on chain.

Mina smart contract computation run off-chain and make it possible to prove that the expected computation was run on private data without revealing the data itself. When the smart contract consumes data from a third-party source, you want to verify that this data is authentic and was provided by the expected source.

The Mina roadmap includes zkOracles to allow a zkApp to consume data trustlessly from any HTTPS data source. The oracle design described in this tutorial is typically operated by the zkApp developer. The oracle fetches and signs the desired data, and then a zkApp can consume this data and verify the signature to ensure that the data was provided by the expected source.

Data providers can also operate as response signers like the one described to provide users with an oracle that does not require them to trust an intermediary. In other words, if a credit score or other data provider chooses to sign response data themselves, users can consume data from that source without trusting anybody besides the data provider they already trust to provide correct data.

Design

This simple oracle design:

  • Fetches data from the desired REST API source
  • Signs it using a Mina-compatible private key
  • Returns the data, signature, and public key associated with the private key
  • Allows the signature to be verified by the zkApp

Code

You can view the complete oracle logic code and the corresponding Next.js project here.

This oracle uses the Vercel Functions. You don't have to dive into the code now, since the code is commented to explain each step so you can build something similar for yourself!

You can adapt this code to create oracles for other API sources. For example, if you want your smart contract to ingest price feed data from an exchange, query the exchange API, sign the results, and return a response in the following response format.

Response Format

The oracle returns a JSON-formatted response with these top-level properties:

  • data: An object of the information you are interested in and can have any form.
  • signature: A signature for the data object created using the oracle operator's private key. Smart contracts use this signature to verify that data was provided by the expected source.
  • publicKey: The public key of the oracle is the same for all requests to this oracle.

The following example is a response from the oracle for the user with the id of 1. In the real world, this id might be a social security number or a similar identifier. Notice that the data property contains their credit score and user id.

The demo oracle for user with id 1 is available at https://07-oracles.vercel.app/api/credit-score?user=1 and shows this response:

{
"data": { "id": 1, "creditScore": 787 },
"signature": "7mXGPCbSJUiYgZnGioezZm7GCy46CEUbgcCH9nrJYXQQiwwVrA5wemBX4T1XFHUw62oR2324QNnkUVXW6yYQLsPsqxZ3nsYR",
"publicKey": "B62qoAE4rBRuTgC42vqvEyUqCGhaZsW58SKVW4Ht8aYqP9UTvxFWBgy"
}

The user with an id of 2 has a credit score that is below the threshold specified in the smart contract. The demo oracle for user with id 2 with a lower credit score is available at https://07-oracles.vercel.app/api/credit-score?user=2 and shows this response:

{
"data": { "id": 2, "creditScore": 536 },
"signature": "7mXXnqMx6YodEkySD3yQ5WK7CCqRL1MBRTASNhrm48oR4EPmenD2NjJqWpFNZnityFTZX5mWuHS1WhRnbdxSTPzytuCgMGuL",
"publicKey": "B62qoAE4rBRuTgC42vqvEyUqCGhaZsW58SKVW4Ht8aYqP9UTvxFWBgy"
}

While the first user has a credit score of 787, the second user has a credit score of 536. The signature is also changed. This makes sense because the payload is different from what is received in the first response. Finally, notice that the publicKey is the same because in each case we are querying data from the same provider.

Generate a key pair for your oracle

You can generate the Mina-compatible public/private key pair for your oracle by executing the following command:

npm run keygen

This command runs the code in the keygen.js file. This file is the part of the oracle Next.js application, source code of which is available here.

Smart Contract

Now that you have an oracle that returns signed data, you can write a smart contract that uses this data.

Create a project

  1. Create or change to a directory where you have write privileges.

  2. Create a project by using the zk project command:

    $ zk project --ui none 07-oracles

This command will scaffold the zkApp project skipping the UI part since we don't need it.

  1. Change into the 07-oracles directory.

For this tutorial, you run commands from the root of the 07-oracles directory.

Each time you make changes, then build or deploy, the TypeScript code is compiled into JavaScript in the build directory.

Prepare the project

The files in the src directory contain the TypeScript code for the smart contract.

  1. Delete the default generated files by running:

    $ rm src/Add.ts
    $ rm src/Add.test.ts
    $ rm src/interact.ts
  2. Create the OracleExample.ts file and generate the corresponding test file:

    $ zk file OracleExample
  3. Change src/index.ts to:

    import { OracleExample } from './OracleExample.js';

    export { OracleExample };

Write the smart contract

You can find the complete code for this smart contract here.

Paste the following content into the src/OracleExample.ts file:

import {
Field,
SmartContract,
state,
State,
method,
PublicKey,
Signature,
} from 'o1js';

// The public key of our trusted data provider
const ORACLE_PUBLIC_KEY =
'B62qoAE4rBRuTgC42vqvEyUqCGhaZsW58SKVW4Ht8aYqP9UTvxFWBgy';

export class OracleExample extends SmartContract {
// Define zkApp state
// Define zkApp events

init() {
// Initialize zkApp state
super.init();
// Specify that caller should include signature with tx instead of proof
this.requireSignature();
}

@method async verify(id: Field, creditScore: Field, signature: Signature) {
// Get the oracle public key from the zkApp state
// Evaluate whether the signature is valid for the provided data
// Check that the signature is valid
// Check that the provided credit score is 700 or higher
// Emit an event containing the verified user's id
}
}

This completes the basic setup for the smart contract. For details on the init() method, see Tutorial 1: Hello World.

On-Chain State

The smart contract stores the public key for the oracle that you retrieve data from as the on-chain state. This makes the public key available when end users run the smart contract. The smart contract then uses this public key to verify the signature of the data to confirm it came from the expected source.

In the src/OracleExample.ts file:

// Define zkApp state
@state(PublicKey) oraclePublicKey = State<PublicKey>();

Use the init method to initialize the oraclePublicKey to the credit score oracle's public key.

init() {
// Initialize zkApp state
super.init();
// Set the oracle public key as zkApp on-chain state
this.oraclePublicKey.set(PublicKey.fromBase58(ORACLE_PUBLIC_KEY));
// Specify that caller should include signature with tx instead of proof
this.requireSignature();
}

Emit Events

The smart contract checks that a user has a credit score above a certain threshold. But, how can the user prove it?

To expose the result to the outside world, you can emit events. Events allow smart contracts to publish arbitrary messages that anybody can verify without requiring them to be stored in the state of a zkApp account. This property makes events ideal for communication with other parties of your application that don't live on-chain, like the UI or even an external service.

This code adds an events object to the smart contract class to define the names and types of the events it can emit:

// Define zkApp events
events = {
verified: Field,
};

Define the verify() method

Next you would like to verify that user's credit score is above 700.

The verify() method is defined like any other TypeScript method, except that it must have the @method decorator in front of it that tells o1js that this method can be invoked by users when they interact with the smart contract.

@method async verify(id: Field, creditScore: Field, signature: Signature) {
...
}

Pass in these arguments:

  • id: The id of the user whose credit score is requested to prevent bad actors from querying somebody else's data and claiming it as their own.
  • creditScore: The credit score of the user that is a number between 350 and 800 (this tutorial uses mock credit scores).
  • signature: A cryptographic signature of oracle's data object (id and creditScore). This is what the smart contract uses to verify that the data was provided by the expected source.

The verify() method does not return any values or change any contract state. It only emits a verified event with the user's id if their credit score is above 700.

Fetch the oracle's public key

To get the oracle's public key from the on-chain state, verify the signature of data from the oracle:

// Get the oracle public key from the zkApp state
const oraclePublicKey = this.oraclePublicKey.get();
this.oraclePublicKey.requireEquals(oraclePublicKey);

The requireEquals() method invocation ensures that the public key that is retrieved at execution time is the same as the public key that exists within the zkApp account on the Mina network when the transaction is processed by the network.

Verify the signature

To ensure that the signature was from our expected source, verify that the signature on the oracle's data object (id and creditScore) is valid for the expected public key. This code returns true if the signature is valid, and false if it is not.

// Evaluate whether the signature is valid for the provided data
const validSignature = signature.verify(oraclePublicKey, [id, creditScore]);

You always want to make it impossible to generate a valid zero knowledge proof if validSignature is false. You can do this with assertTrue(). If the signature is invalid, this throws an exception and makes it impossible to generate a valid zero knowledge proof and proceed with transaction creation.

// Check that the signature is valid
validSignature.assertTrue();

Verify the credit score is 700 or higher

You want the verify() method to emit an event only if the user's credit score is 700 or higher. To ensure that this condition is met, call assertGreaterThanOrEqual() (assert greater than or equal to) on creditScore.

// Check that the provided credit score is 700 or higher
creditScore.assertGreaterThanOrEqual(Field(700));

These assert methods create a constraint that makes it impossible for users to generate a valid zero knowledge proof unless their condition is met. Without a valid zero knowledge proof (or a signature) it's impossible to generate a valid Mina transaction. Users can call the smart contract method and send a valid transaction only if they have a valid signature from the expected oracle and a credit score 700 or above.

Emit a verified event

With this foundation, you can emit a verified event.

  • The first argument to emitEvent() is an arbitrary string name, because a smart contract could emit more than one type of event.
  • The second argument can be any value, as long as it matches the type defined for the event.

In this case, the event has the Field type, but it could be a more complicated type built on Fields, if the situation called for it. Emitted events can be fetched using the Archive-Node-API.

// Emit an event containing the verified user's id
this.emitEvent('verified', id);

Test your smart contract

You can find the complete code for the smart contract tests here.

When you ran the zk file OracleExample command, the zkApp CLI automatically generated a test file src/OracleExample.test.ts.

To add tests, paste the following code in the OracleExample.test.ts file:

import { OracleExample } from './OracleExample';
import {
Field,
Mina,
PrivateKey,
PublicKey,
AccountUpdate,
Signature,
} from 'o1js';

let proofsEnabled = false;

// The public key of our trusted data provider
const ORACLE_PUBLIC_KEY =
'B62qoAE4rBRuTgC42vqvEyUqCGhaZsW58SKVW4Ht8aYqP9UTvxFWBgy';

describe('OracleExample', () => {
let deployerAccount: Mina.TestPublicKey,
deployerKey: PrivateKey,
senderAccount: Mina.TestPublicKey,
senderKey: PrivateKey,
zkAppAddress: PublicKey,
zkAppPrivateKey: PrivateKey,
zkApp: OracleExample;

beforeAll(async () => {
if (proofsEnabled) await OracleExample.compile();
});

beforeEach(async () => {
const Local = await Mina.LocalBlockchain({ proofsEnabled });
Mina.setActiveInstance(Local);
deployerAccount = Local.testAccounts[0];
deployerKey = deployerAccount.key;
senderAccount = Local.testAccounts[1];
senderKey = senderAccount.key;
zkAppPrivateKey = PrivateKey.random();
zkAppAddress = zkAppPrivateKey.toPublicKey();
zkApp = new OracleExample(zkAppAddress);
});

async function localDeploy() {
const txn = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount);
await zkApp.deploy();
});
await txn.prove();
// this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
await txn.sign([deployerKey, zkAppPrivateKey]).send();
}

it('generates and deploys the `OracleExample` smart contract', async () => {
await localDeploy();
const oraclePublicKey = zkApp.oraclePublicKey.get();
expect(oraclePublicKey).toEqual(PublicKey.fromBase58(ORACLE_PUBLIC_KEY));
});

describe('hardcoded values', () => {
it('emits an `id` event containing the users id if their credit score is above 700 and the provided signature is valid', async () => {
await localDeploy();

const id = Field(1);
const creditScore = Field(787);
const signature = Signature.fromBase58(
'7mXGPCbSJUiYgZnGioezZm7GCy46CEUbgcCH9nrJYXQQiwwVrA5wemBX4T1XFHUw62oR2324QNnkUVXW6yYQLsPsqxZ3nsYR'
);

const txn = await Mina.transaction(senderAccount, async () => {
await zkApp.verify(id, creditScore, signature);
});
await txn.prove();
await txn.sign([senderKey]).send();

const events = await zkApp.fetchEvents();
const verifiedEventValue = events[0].event.data.toFields(null)[0];
expect(verifiedEventValue).toEqual(id);
});

it('throws an error if the credit score is below 700 even if the provided signature is valid', async () => {
await localDeploy();

const id = Field(1);
const creditScore = Field(536);
const signature = Signature.fromBase58(
'7mXXnqMx6YodEkySD3yQ5WK7CCqRL1MBRTASNhrm48oR4EPmenD2NjJqWpFNZnityFTZX5mWuHS1WhRnbdxSTPzytuCgMGuL'
);

expect(async () => {
const txn = await Mina.transaction(senderAccount, async () => {
await zkApp.verify(id, creditScore, signature);
});
}).rejects;
});

it('throws an error if the credit score is above 700 and the provided signature is invalid', async () => {
await localDeploy();

const id = Field(1);
const creditScore = Field(787);
const signature = Signature.fromBase58(
'7mXPv97hRN7AiUxBjuHgeWjzoSgL3z61a5QZacVgd1PEGain6FmyxQ8pbAYd5oycwLcAbqJLdezY7PRAUVtokFaQP8AJDEGX'
);

expect(async () => {
const txn = await Mina.transaction(senderAccount, async () => {
await zkApp.verify(id, creditScore, signature);
});
}).rejects;
});
});

describe('actual API requests', () => {
it('emits an `id` event containing the users id if their credit score is above 700 and the provided signature is valid', async () => {
await localDeploy();

const response = await fetch(
'https://07-oracles.vercel.app/api/credit-score?user=1'
);
const data = await response.json();

const id = Field(data.data.id);
const creditScore = Field(data.data.creditScore);
const signature = Signature.fromBase58(data.signature);

const txn = await Mina.transaction(senderAccount, async () => {
await zkApp.verify(id, creditScore, signature);
});
await txn.prove();
await txn.sign([senderKey]).send();

const events = await zkApp.fetchEvents();
const verifiedEventValue = events[0].event.data.toFields(null)[0];
expect(verifiedEventValue).toEqual(id);
});

it('throws an error if the credit score is below 700 even if the provided signature is valid', async () => {
await localDeploy();

const response = await fetch(
'https://07-oracles.vercel.app/api/credit-score?user=2'
);
const data = await response.json();

const id = Field(data.data.id);
const creditScore = Field(data.data.creditScore);
const signature = Signature.fromBase58(data.signature);

expect(async () => {
const txn = await Mina.transaction(senderAccount, async () => {
await zkApp.verify(id, creditScore, signature);
});
}).rejects;
});
});
});

To run the tests:

  1. Save the OracleExample.test.ts file.
  2. Run npm i.
  3. Run npm run test.

Note that writing a test that calls an API is generally not a best practice, but it's convenient for the sake of this tutorial. You can also mock your HTTP requests.

Congratulations! You have just built a simple oracle using o1js and the Mina blockchain. You can find the complete code for this example here.