Skip to content

Request a Proof

The interface for developers to use the Boundless core services is the Boundless Market, developers interact with it by sending a proof request. This can be done programmatically using the boundless-market crate or with the Boundless CLI.

Programmatically

The recommended flow of requesting a proof is:

  1. Create a boundless client.
  2. Upload the program executable (ELF binary) to IPFS.
  3. Upload the input of your program to IPFS.
  4. Execute the program to estimate the total number of cycles, and to get the journal.
  5. Create a proving request with URLs to the executable and inputs, the journal hash, the imageID, and the offer details (such as price range and timeout length).
  6. Submit the request to the market.
  7. Wait for the request to be fulfilled, returning the journal and the seal.

The Boundless Foundry Template provides a full code example of this workflow in apps/src/main.rs.

1. Create a Boundless Client

To facilitate communication with the Boundless market, developers can use the Client struct from the boundless-market crate. Client manages interaction with the proof market contract and handles the necessary authentication and transaction signing.

To instantiate a boundless client, you will need a wallet with sufficient funds on Sepolia, a working Sepolia RPC provider, and the proof market and set verifier contract addresses:










let boundless_client = ClientBuilder::default()
  .with_rpc_url(rpc_url)
  .with_boundless_market_address(boundless_market_address)
  .with_set_verifier_address(set_verifier_address)
  .with_order_stream_url(order_stream_url)
  .with_storage_provider_config(storage_config.clone())
  .with_private_key(private_key)
  .build()
  .await?;

For the latest deployment address, please see Deployments.

Relevant links: Sepolia Faucet, Sepolia Public RPCs, Boundless Deployments Reference

2. Upload Program Executable to IPFS

The ELF binary, also known as the image, of the program should be accessible to provers. This is done by uploading the ELF to a public HTTP server. The boundless-market provides StorageProvider implementations to help upload these binaries. Built-in providers are listed in the BuiltinStorageProvider.

The recommended storage provider is IPFS, specifically through Pinata (the free tier should be sufficient for most Boundless use cases). To use Pinata, fetch the required JWT credential and set it to the PINATA_JWT environment variable in the .env file.

With the storage provider added to the Client, the executable can be uploaded with the upload_image method:




let image_url = boundless_client.upload_image(ELF).await?;

3. Upload the Input of Your Program to IPFS

To execute and run proving, the prover requires the inputs of the program. Similar to the program executable, the program inputs are uploaded to a public HTTP server, and the BuiltinStorageProvider can help with that.




let image_url = boundless_client.upload_input(&input_bytes).await?;

Inputs can also be included directly into the request instead as in-line input. When submitting requests onchain, this will cost more gas if the inputs are large. The off-chain order-stream service also places limits on the size of in-line input.

Relevant links: BuiltInStorageProvider, Pinata, Get JWT Credentials from Pinata, upload_image for PinataStorageProvider

4. Execute the Program to Estimate Cycles and to Get the Journal

Before sending a proof request, developers should execute the program. This achieves a few separate goals:

  • It estimates the number of cycles in the program, which can be used to estimate the cost of the proof request offer.
  • It makes sure that the program executes without errors, saving the developer money by not sending unprovable proof requests to the market.
  • It retrieves the journal, the public outputs of the program, which are used to check proof validity along with the imageID.

To execute the program and retrieve the cycle count (in MCycles), and journal:



let env = ExecutorEnv::builder().write_slice(&input_bytes).build()?;
let session_info = default_executor().execute(env, ELF)?;
let mcycles_count = session_info
  .segments
  .iter()
  .map(|segment| 1 << segment.po2)
  .sum::<u64>()
  .div_ceil(1_000_000);
let journal = session_info.journal;

Relevant links: ExecutorEnvBuilder

5. Create a Proof Request

A proof request consists of four essential components that specify which program needs to be proven and under what conditions:











let request = ProofRequest::default()
  .with_image_url(&image_url)
  .with_input(Input::url(&input_url))
  .with_requirements(Requirements::new(ECHO_ID, Predicate::digest_match(journal.digest())))
  .with_offer(
    Offer::default()
      // The market uses a reverse Dutch auction mechanism to match requests with provers.
      // Each request has a price range that a prover can bid on. One way to set the price
      // is to choose a desired (min and max) price per million cycles and multiply it
      // by the number of cycles. Alternatively, you can use the `with_min_price` and
      // `with_max_price` methods to set the price directly.
      .with_min_price_per_mcycle(parse_ether("0.001")?, mcycles_count)
      // NOTE: If your offer is not being accepted, try increasing the max price.
      .with_max_price_per_mcycle(parse_ether("0.002")?, mcycles_count)
      // The timeout is the maximum number of blocks the request can stay
      // unfulfilled in the market before it expires. If a prover locks in
      // the request and does not fulfill it before the timeout, the prover can be
      // slashed.
      .with_timeout(1000),
  );

  1. Program Image (with_image_url) (line #2)
  2. Program Input (with_input) (line #3)
  3. Requirements (with_requirements) (line #4)
  4. Offer (with_offer) (line #5)

The program image and program inputs are discussed in steps 2 and 3 respectively.

Proof Requirements

The requirements ensure proof integrity and correctness by checking the image ID (a unique identifier of each ELF binary run in the zkVM) and the hash of the journal. By checking both the image ID and the journal hash, we can be sure that the provers are working with the correct program and that the outputs match the expected outputs (the ones generated by the execution in step 4).

Offer Details

The offer details are specified with .with_offer(). This allows the requestor to set the price range per million cycles (MCycles), and long the request remains valid (known as the timeout). The price mechanism helps match the request with provers.

Relevant links: Cycles, ELF Binary

6. Submit a Request

Once the request is configured, it can submitted to the market:





let (request_id, expires_at) = boundless_client.submit_request(&request).await?;

This will send the request onchain in a transaction. Using boundless_client.submit_request_offchain, you can send your request off-chain through the order-stream service. Off-chain requests will not cost any gas, but does require you to deposit funds to the market first.

The submit_request method returns a request_id which can be used to track the proof request (see Boundless Indexer).

7. Wait for the Request to Be Fulfilled

Once fulfilled, the journal and seal are returned. The journal contains the public outputs of the guest program, and the seal.







let (_journal, seal) = boundless_client
  .wait_for_request_fulfillment(
    request_id,
    Duration::from_secs(5), // check every 5 seconds
    expires_at,
  )
  .await?;

Relevant links: Seal, Journal

Requesting a Proof via the Boundless CLI

In early testing, and when trying out new order parameters, it can be useful to submit a request via the Boundless CLI.

Installing the Boundless CLI

The Boundless CLI can be installed using Cargo.

You'll need to install Rust, then you can run the following command to install the CLI.

Terminal
cargo install --locked boundless-cli

Boundless CLI

The Boundless CLI builds upon the boundless_market library. It covers multiple market interactions such as submitting proof requests, cancelling requests, executing dry runs, requesting the status of a given request, retrieving the journal and seal of a fulfilled request and verifying a proof.

To submit a proof, a valid request.yaml is required, this config file will specify the parameters of the request:

  • Request ID
    • This can be specified, or if set to 0, a random ID will be assigned.
  • Requirements:
    • The image ID of the program being proven.
    • The contents of the outputs of the program, the journal. This is to make sure the outputs are as expected (e.g. to ensure the right input was provided, by checking an input digest committed to the journal).
  • Image URL
    • The link to the ELF binary stored on any public HTTP server. IPFS, used through a gateway, works well (Boundless will support IPFS URLs natively in the future).
  • Input:
    • The input bytes are passed to the program for execution. The input can have any encoding. The bytes will be passed to the guest without modification.
  • Offer:
    • This includes the minimum and maximum price for the proof request, the block number to open bidding, the price ramp up period, how many blocks before the request should timeout, and the lock-in stake the prover has to escrow to submit a bid.

Below is an example of a request.yaml file that can be used with the boundless-cli submit-request command.

request.yaml
# Unique ID for this request, constructed from the client address and a 32-bit index.
# Constructed as (address(client) << 32) | index
id: 0 # if set to 0, gets overwritten by a random id
 
# Specifies the requirements for the delivered proof, including the program that must be run,
# and the constraints on the journal's value, which define the statement to be proven.
requirements:
  imageId: "722705a82a1dab8369b17e16bac42c9c538057fc1d32933d21ea2b47f292efb4"
  predicate:
    predicateType: PrefixMatch
    data: "57656420"
 
# A public URI where the program (i.e. image) can be downloaded. This URI will be accessed by
# provers that are evaluating whether to bid on the request.
imageUrl: "https://dweb.link/ipfs/QmTx3vDKicYG5RxzMxrZEiCQJqhpgYNrSFABdVz9ri2m5P"
 
# Input to be provided to the zkVM guest execution.
input:
  inputType: Inline
  data: "576564204a756c2020332031343a33373a31322050445420323032340a"
 
# Offer specifying how much the client is willing to pay to have this request fulfilled
offer:
  minPrice: 1000000000000000 # 0.001 Sepolia ETH
  maxPrice: 2000000000000000 # 0.002 Sepolia ETH
  biddingStart: 0 # if set to 0, gets overwritten by the current block height
  rampUpPeriod: 100
  timeout: 300 # 1 hour at 12s block times.
  lockinStake: 100000000000000

To submit a request, export or create a .env file with the following environment variables:

Terminal
export RPC_URL="https://ethereum-sepolia-rpc.publicnode.com"
export PRIVATE_KEY="YOUR_SEPOLIA_WALLET_PRIVATE_KEY"
export BOUNDLESS_MARKET_ADDRESS="0x01e4130C977b39aaa28A744b8D3dEB23a5297654"
export SET_VERIFIER_ADDRESS="0xea6a0Ca4BfD0A6C43081D57672b3B6D43B69265F"
export ORDER_STREAM_URL="https://order-stream.beboundless.xyz"

Then run the following command:

Terminal
RUST_LOG=info boundless-cli submit-request request.yaml

To wait until the submitted request has been fulfilled, the --wait option can be added:

Terminal
RUST_LOG=info boundless-cli submit-request request.yaml --wait

And to submit the request to the off-chain order-stream service, make a deposit and then run submit-request with --offchain.

Terminal
RUST_LOG=info boundless-cli deposit 0.002 # Enough for the request above; deposit more to cover multiple requests.
RUST_LOG=info boundless-cli submit-request request.yaml --wait --offchain

Pricing A Request

The request.yaml specifies the details of the offer. This section will cover these offer details and the interactions between the requestor and the prover in the early stages of the proof request, i.e. price auction, bid submission and prover lock in.

An offer contains the following:

  • Pricing parameters
  • Minimum price
  • Maximum price
  • Bidding start (defined as a block number)
  • Length of ramp-up period (measured in blocks since the start of the bid)
  • Timeout (measured in blocks since the start of the bid)
  • Lock-in stake

For example, an offer might specify:

  • Pricing parameters
    • Minimum price: 0.001 Ether
    • Maximum price: 0.002 Ether
  • Bidding start: Block number 1000
  • Length of ramp-up period: 5 blocks
  • Timeout: 100 blocks
  • Lock-in stake: 0.4 Ether

The pricing parameters are used to determine the reward that gets paid-out when the request is fulfilled (ie, the proof has been verified). The reward is governed by the price function. Its inputs are:

  • The offer.
  • The number of blocks that have passed since the bidding started.

The function works like so:

  • During the discovery period (the initial phase of the auction before bidding start), the price is just the minimum price.
  • During the ramp-up period (which immediately follows the discovery period), the price grows linearly up-to the maximum price.
  • After the ramp-up period, the price is just the maximum price.

Continuing with the example offer given above, the price is constant (0.001 Ether, its minimum) for the first 10 blocks; on block 11 it jumps to 0.0012 Ether; on 12 it jumps to 0.0014 Ether; on 15 it reaches 0.002 Ether (its maximum), and remains at that value until the offer expires.

When a prover locks-in a request, they are agreeing to be paid the reward offered by this function at the time of their bid.

📟 Pricing Calculator

Use this interactive calculator to get suggested request parameters based on your program's complexity and desired proof time:

Troubleshooting a Request

If you submitted a request, and it is stuck in an "Unknown" / "Submitted" state, there may be something that is preventing the provers from picking up your request.

  • The guest may be panicking or otherwise failing to complete the job.
    • Pre-flight your request locally before sending it.
    • The boundless-cli does this by default, and the execution step in the Foundry template also accomplishes this.
    • If your request is submitted onchain, you can use the following command to execute it locally. If it succeeds, this is not the issue.
Terminal
RUST_LOG=info boundless-cli execute --request-id $REQUEST_ID
  • The offer price may be too low for the size of the job.
    • Try increasing the max price and ramp-up period.
    • The auction mechanism will ensure that the price you pay is the lowest price any online prover is willing to pay.
  • The lock-in stake may be too high.
  • The timeout may be too short.
  • The bidding start block may be in the past, causing the request to immediately expire.
  • The ELF or input URLs may not be accessible to the prover.