This repository implements an example application using InfinityVM. InfinityVM enables developers to use expressive offchain compute to build and enhance their EVM applications.
This repo contains three folders:
programs
: Rust programs that contain application logic to be run offchain in the coprocessor.contracts
: ASquareRootConsumer
contract for the application, contracts for the coprocessor, and tests and a deploy script for the contracts.- To build on InfinityVM, you just need to read the
SquareRootConsumer.sol
andSquareRootConsumer.t.sol
files. The coprocessor contracts incontracts/coprocessor
expose an interface you can use but you don't need to read how they're implemented.
- To build on InfinityVM, you just need to read the
zkvm-utils
: Utility functions for InfinityVM. You don't need to read these files to build on InfinityVM.
The flow of the InfinityVM coprocessor looks like this:
- An app contract or an offchain user requests a compute job from the coprocessor.
- The coprocessor executes this job and submits the result back to the contract.
- The app contract can simply use the result from the coprocessor in any of their app logic.
This section will take us through an example of building an app that computes and stores the square root of numbers.
Clone this repo (including submodules):
git clone --recursive https://github.com/InfinityVM/infinityVM-foundry-template.git
Setup rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup use 1.80
Install Foundry:
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge build
Next, install the SP1 toolchain for zkVM functionality:
curl -L https://sp1.succinct.xyz | bash
sp1up
# Verify the installation
cargo prove --version
All application programs run by the coprocessor live in programs/
. Each program has its own folder within programs/
. For example, for our square root app, we have this:
programs
├── square-root // Name of the program
│ ├── src
│ ├── main.rs // Core logic of the program
│ ├── Cargo.lock
│ └── Cargo.toml
The app's program is main.rs
, which takes in an integer and returns the square root. This program is also a good example of how to accept inputs and return output.
This is a simple example but you could write a lot more interesting and complex code in your Rust programs. One thing to note is you can't print anything to stdout
in your Rust program (if you'd like to print something while debugging your Rust program, we've provided instructions in the Write tests for your app
section below).
After you've written your Rust program, add its name to the PROGRAM_NAMES
list in programs/build.rs
. For example, we currently have:
// Add your zkVM programs here.
const PROGRAM_NAMES: &[&str] = &["square-root"];
Now you can run:
cargo build
This will build your program and update the relevant contracts to allow you to use your program from the contracts. Every program has a unique program ID generated for it, which is added to the ProgramID.sol
contract.
We have a contract for the square root app in contracts/src/SquareRootConsumer.sol
.
We can call the square_root.rs
program from our app contract. We just need to do two things:
- Call
requestJob()
with the program ID ofsquare_root.rs
fromProgramID.sol
along with ABI-encoded inputs (the number we want to calculate the square root of). - Write a
_receiveResult()
function which accepts the output from thesquare_root.rs
program and uses it in some application logic.
To build the contracts, you can run:
forge build
We can also call the square_root.rs
program offchain by sending a request directly to the coprocessor. The coprocessor will execute the job and submit the result to our app contract. The flow looks like this:
The offchain request to the coprocessor can be sent by an app, a user, or any authorized third-party. To support offchain requests for your app contract, you need to implement the isValidSignature()
function in your contract, which is called to verify whether an offchain request is signed by an authorized signer/user. We've provided an example implementation of isValidSignature()
in the SquareRootConsumer.sol
contract (which checks that each job request is signed by a signer owned by the app), but you can implement any logic or checks you'd like.
To test this flow, you can call requestOffchainJob()
in the tests. More instructions on how to write tests are in the Write tests for your app
section below.
We have two end-to-end tests for the SquareRootConsumer
app in SquareRootConsumer.t.sol
:
test_Consumer_RequestJob()
: This test requests the square root of a number from theSquareRootConsumer.sol
contract. It verifies that the contract calls thesquare_root.rs
program and that the coprocessor submits the correct result back to the contract.test_Consumer_RequestOffchainJob()
: This test sends an offchain request for the square root of a number directly to the coprocessor, usingrequestOffchainJob()
. It verifies that the coprocessor submits the correct result back to theSquareRootConsumer.sol
contract.
You can add any tests for your app contracts in the SquareRootConsumer.t.sol
file.
To run the tests, you can run:
forge test -vvv --ffi
If you would like to write unit tests for your Rust program or debug your program by itself, we have an example test in programs/src/lib.rs
. You can run this using:
cargo test
You can add println!
statements to your Rust program to help while debugging.
Feel free to reach out to our team if you have any questions, we're happy to help!
Each job request for an app contract must have a unique nonce
submitted with it, to prevent replay attacks. The Consumer.sol
contract contains a getNextNonce()
function to return the next nonce to be used by job requests from the contracts and offchain users, and an updateLatestNonce()
function to update the latest nonce value once a job has been submitted.
We have provided a default implementation for getNextNonce()
and updateLatestNonce()
in Consumer.sol
to implement a simple nonce which increases by 1 every time a job is requested. This should be good enough for most apps, but you can override it in your consumer contract if you'd like. For example, you could use the unix timestamp in milliseconds as the nonce for offchain calls to the coprocessor.
With InfinityVM, some apps can leverage offchain job requests to run as real-time servers. Because of the limitations of foundry, you can't build and test an app server using the foundry template. Instead, you would need to write tests for your app server similar to how you would write end-to-end tests for any multi service setup. We have example end-to-end tests for app servers in the InfinityVM repo.
Lint:
cargo clippy
Format:
cargo +nightly fmt