zkApp programmability is not yet available on the Mina Mainnet, but zkApps can now be deployed on Berkeley Testnet.
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 build an oracle that retrieves data from a REST API and write a smart contract that consumes information from this oracle.
- Retrieve data from a REST API that provides mock credit score information for two users: one with a high credit score and one with a low credit score.
- 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, an end user can generate an attestation that their credit score is above a certain value. To maintain their privacy, the user 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, a REST API, for example.
How Oracles Work
Oracles connect blockchain smart contracts with the outside world to get data on chain.
Mina smart contracts 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 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 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 code.
This oracle uses a Vercel Serverless Function. 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 thedata
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 an 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 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 demo oracle for user 2 with a lower credit score is available at https://07-oracles.vercel.app/api/credit-score?user=2 and shows this response:
The user with an id
of 2
has a credit score that is below the threshold specified in the smart contract.
{
"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
To generate a Mina-compatible public/private key pair for your oracle:
npm run keygen
This command runs the code in the keygen.js file.
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
Create or change to a directory where you have write privileges.
Create a project by using the
zk project
command:
$ zk project 07-oracles
The zk project
command has the ability to scaffold the UI for your project. For this tutorial, select none
:
? Create an accompanying UI project too? …
next
svelte
nuxt
empty
❯ none
- Change into the
07-oracles
directory.
For this tutorial, you run commands from the root of the 07-oracles
directory.
Each time you make updates, 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.
- Delete the default generated files by running:
$ rm src/Add.ts
$ rm src/Add.test.ts
$ rm src/interact.ts
- Create the
OracleExample.ts
file and generate the corresponding test file:
$ zk file OracleExample
- Change
index.ts
to:
import { OracleExample } from './OracleExample.js';
export { OracleExample };
Write the smart contract
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 contract state
// Define contract events
init() {
super.init();
// Initialize contract state
// 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 contract state
// Evaluate whether the signature is valid for the provided data
// Check that the signature is valid
// Check that the provided credit score is greater than 700
// Emit an event containing the verified users 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 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 contract state
@state(PublicKey) oraclePublicKey = State<PublicKey>();
Use the init
method to initialize oraclePublicKey
to the credit score oracle's public key.
init() {
super.init();
// Initialize contract 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 that them to be stored in the state of a zkApp account. This property makes events ideal for communication with other parts 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 contract events
events = {
verified: Field,
};
Define the verify() method
To verify a that user's credit score is above 700, add a method.
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 is required 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 ofid
andcreditScore
. 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 contract state
const oraclePublicKey = this.oraclePublicKey.get();
this.oraclePublicKey.requireEquals(oraclePublicKey);
requireEquals()
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 the signature was from our expected source, verify that the signature on the data (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 transaction.
// 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 greater than 700
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 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 is Field
, but it could be a more complicated type built on Fields, if the situation called for it. Emitted events are stored and available on archive nodes in the Mina network.
// Emit an event containing the verified users id
this.emitEvent('verified', id);
Test your smart contract
When you ran the zk file OracleExample
command, the zkApp CLI automatically generated a test file called 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: PublicKey,
deployerKey: PrivateKey,
senderAccount: PublicKey,
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:
- Save the
OracleExample.test.ts
file. - 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 Protocol. You can find the complete code for this example here.