Skip to content

Build a Program

At its core, Boundless allows app developers to receive zero-knowledge (ZK) proofs for their programs. Before requesting a proof, developers should have a program that compiles and executes successfully.

If you want to read more about the zkVM, it is highly recommended to read the following pages on the zkVM dev docs:

For some more intermediate and advanced reading on the zkVM, please refer to:

Boundless Foundry Template

The Boundless Foundry Template builds on RISC Zero's Foundry Template to incorporate Boundless with a simple example app. It consists of three main parts:

The example app executes the following steps:

  1. Uploads the guest program and creates the proof request.
  2. Sends the proof request to Boundless.
  3. Retrieves the proof from Boundless.
  4. Sends the proof to the application contract.
Boundless Foundry Template

The zkVM program for this example app is a simple program that takes an input number, checks if it is even and if so, outputs the number to the public outputs of the computation (known as the journal).

The entire program is only ~20 lines, so let's run through it:

is-even

use std::io::Read;
 
use alloy_primitives::U256;
use alloy_sol_types::SolValue;
use risc0_zkvm::guest::env;
 
fn main() {
    // Read the input data for this application.
    let mut input_bytes = Vec::<u8>::new();
    env::stdin().read_to_end(&mut input_bytes).unwrap();
 
    // Decode and parse the input
    let number = <U256>::abi_decode(&input_bytes, true).unwrap();
 
    // Run the computation.
    // In this case, asserting that the provided number is even.
    assert!(!number.bit(0), "number is not even");
 
    // Commit the journal that will be received by the application contract.
    // Journal is encoded using Solidity ABI for easy decoding in the app contract.
    env::commit_slice(number.abi_encode().as_slice());
}

First, the guest receive inputs from the host program, and then these input bytes are decoded and parsed:





 
fn main() {
    // Read the input data for this application.
    let mut input_bytes = Vec::<u8>::new(); 
    env::stdin().read_to_end(&mut input_bytes).unwrap(); 
 
    // Decode and parse the input
    let number = <U256>::abi_decode(&input_bytes, true).unwrap(); 
    ....
}

To check if the number is even:





 
fn main() {



    ....
    // Run the computation.
    // In this case, asserting that the provided number is even.
    assert!(!number.bit(0), "number is not even"); 
    ....
}

Finally, if the number is even, the assert will pass and the number can be committed to the journal.





 
fn main() {



    ....
    // Commit the journal that will be received by the application contract. [!code focus]
    // Journal is encoded using Solidity ABI for easy decoding in the app contract. [!code focus]
    env::commit_slice(number.abi_encode().as_slice()); 
    ....
}

EvenNumber.sol

The EvenNumber smart contract holds a uint256 variable called number. This number is guaranteed to be even, however the smart contract itself never checks the number's parity directly. It verifies a ZK proof of a program that has checked if the number is even. This is done in the set function:

/// @notice Set the even number stored on the contract. Requires a RISC Zero proof that the number is even.
function set(uint256 x, bytes calldata seal) public {
    // Construct the expected journal data. Verify will fail if journal does not match.
    bytes memory journal = abi.encode(x);
    verifier.verify(seal, imageId, sha256(journal));
    number = x;
}

This set function is called with two arguments, a number x, and the seal. The number x is the number that the ZK proof has proven is even, and it is used to reconstruct the journal (the journal refers to the public outputs of the program). The seal is the proof. In zkVM terminology, the seal usually refers to a zk-STARK or SNARK directly. In Boundless, the seal is actually a Merkle inclusion proof that the program's proof verification has been included in the Batch Verification Tree. See Proof Lifecycle to read more about this.

Proof Verification Using the VerifierRouter Contract

The main logic of this function is carried out with verifier.verify:

/// @notice Set the even number stored on the contract. Requires a RISC Zero proof that the number is even.
function set(uint256 x, bytes calldata seal) public {
    // Construct the expected journal data. Verify will fail if journal does not match.
    bytes memory journal = abi.encode(x); 
    verifier.verify(seal, imageId, sha256(journal)); 
    number = x;
}
  • Verifier
    • The verifier variable points to the RISC Zero Verifier Router contract. This contract supports verification of seals that are either proofs returned from Boundless or zkSNARKs directly from the zkVM.
  • ImageID
    • The image ID is a unique identifier for a program in the zkVM. It is checked during proof verification to ensure proof integrity. Concretely, by saving the image ID to an immutable variable in the smart contract on deployment, this makes sure that only proofs from the correct program (in this case, is-even) are valid.
  • Journal
    • The journal refers to the public outputs of the program. Similar to the image ID, the journal ensures proof consistency by verifying that the journal in the receipt claim (the seal cryptographically attests to the receipt claim) matches the expected journal.

apps/src/main.rs

As an example of how to request and publish a proof an example app is provided. It is a CLI that takes a --number argument, and does all the work required to set a new even number on the contract.

The example app executes the following steps:

  1. Uploads the guest program and creates the proof request.
  2. Sends the proof request to Boundless.
  3. Retrieves the proof from Boundless.
  4. Sends the proof to the application contract.

Instructions for running this example can be found in the Boundless Foundry Template.

For a detailed walkthrough of this app file, please refer to Request A Proof, where this program is explained in detail.