Skip to content

Request a Proof

The Boundless Market SDK allows developers to build and submit requests to the Boundless protocol; the SDK has sensible defaults, designed to make sending ~95% of requests straightforward.

Therefore, this page is split into two sections:

  • The first section, Sending A Request, shows the quickest and easiest way to request a proof using these sensible defaults, without any additional configuration.
  • The second section, Request Configuration, covers all available configuration options for the 5% of requests that require fine-tuning.

Sending a Request

1. Setting environment variables

Blockchain

Since we are submitting requests onchain, we will need private key for a wallet with sufficient funds on Sepolia, and a working RPC URL:

export RPC_URL="https://..."
export PRIVATE_KEY="abcdef..."

Storage Provider

To make a program, and its inputs, accessible to provers, they need to be hosted at a public URL. We recommend using IPFS for storage, particularly via Pinata, as their free tier comfortably covers most Boundless use cases.

Before submitting a request, you'll need to:

  • Sign up for an account with Pinata.
  • Generate an API key following their documentation.
  • Copy the JWT token and set it as the PINATA_JWT environment variable:
export PINATA_JWT="abcdef..."

2. Build the Boundless Client
















let client = Client::builder()
  .with_rpc_url(args.rpc_url)
  .with_private_key(args.private_key)
  .with_storage_provider(Some(storage_provider_from_env()?))
  .build()
  .await?;

3. Create and Submit a Proof Request










// Create a request using new_request
let request = client.new_request().with_program(ECHO_ELF).with_stdin(echo_message.as_bytes());
 
// Submit the request onchain, via a transaction
let (request_id, expires_at) = client.submit_onchain(request).await?;

4. Retrieve the Proof

Once submitted, you can keep track of the request using:









// Wait for the request to be fulfilled. The market will return the journal and seal.
tracing::info!("Waiting for request {:x} to be fulfilled", request_id);
let (journal, seal) = client
    .wait_for_request_fulfillment(
        request_id,
        Duration::from_secs(5), // check every 5 seconds
        expires_at,
    )
    .await?;
tracing::info!("Request {:x} fulfilled", request_id);

This will store the journal and seal from the Boundless market, together they represent the public outputs of your guest and the proof itself, respectively. You can use a proof in your application to access the power of verifiable compute using Boundless.

Request Configuration

Storage Providers

The Boundless Market SDK automatically configures the storage provider based on environment variables; it supports both IPFS and S3 for uploading programs and inputs.

IPFS

For example, if you set the following:

export PINATA_JWT="abcdef"...

then when you use .with_storage_provider():
















let client = Client::builder()
  .with_rpc_url(args.rpc_url)
  .with_private_key(args.private_key)
  .with_storage_provider(Some(storage_provider_from_env()?)) 
  .build()
  .await?;

IPFS is set automatically to the storage provider, and your JWT will be used to upload programs/inputs via Pinata's gateway.

S3

To use S3 as your storage provider, you need to set the following environment variables:

export S3_ACCESS_KEY="abcdef..."
export S3_SECRET_KEY="abcdef..."
export S3_BUCKET="bucket-name..."
export S3_URL="https://bucket-url..."
export AWS_REGION="us-east-1"

Once these are set, this will automatically use the specified AWS S3 bucket for storage of programs and inputs.

No Storage Provider

A perfectly valid option for StorageProvider is None; if you don't set any relevant environment variables for IPFS/S3, it won't use a storage provider to upload programs or inputs at runtime. This means you will need to upload your program ahead of time, and provide the public URL. For the inputs, you can also pass them inline (i.e. in the transaction) if they are small enough. Otherwise, you can upload inputs ahead of time as well.

Uploading Programs

Provers must be able to access your guest program via a publicly accessible URL; the Boundless Market SDK allows you to directly upload your program in a few different ways.

Manually



let client = Client::builder()
  .with_storage_provider(Some(storage_provider_from_env()?))
  .build()
  .await?;
let program_url = client.upload_program(program).await?;

After which, you'd create a request with:






let request = client.new_request()
  .with_program_url(program_url)?
  .with_input_url(input_url);

If you already have the program_url, you do not need to upload the program again; you can simply use with_program_url with a hard-coded URL.

Automagically

If you are working in a monorepo (i.e. your zkVM host/guest is in the same repo), you can take advantage of risc0-build which automatically builds and exposes the ELF for the guest. The counter example uses this method:









// Import ECHO_ELF from your guest code
use guest_util::{ECHO_ELF};
// Create a request using new_request
let request = client.new_request()
  .with_program(ECHO_ELF)
  .with_stdin(b"Hello, world!");

Inputs

To execute and run proving, the prover requires the inputs of the program. Inputs can be provides as a public URL, or "inline" by including them directly in the request.

Program inputs are uploaded to the same storage provider. This can be done manually like so:







let input_url = client.upload_input(&input_bytes).await?;

or if we look back at the counter example, we can see that the inputs are included directly into the request builder:









// Create a request using new_request
let request = client.new_request().with_program(ECHO_ELF).with_stdin(echo_message.as_bytes()); 
 
// Submit the request directly
let (request_id, expires_at) = client.submit_onchain(request).await?;

In this example, inputs are included inline if they are small (e.g. less than 1 kB) or uploaded to a public URL first if they are large.

When submitting requests onchain with inline inputs, this will cost more gas if the inputs are large. The offchain order-stream service also places limits on the size of inline input.

Onchain vs Offchain

The Boundless protocol allows you to submit requests both onchain and offchain.

Onchain

To submit a request onchain, we use:









// Create a request using new_request
let request = client.new_request().with_program(ECHO_ELF).with_stdin(echo_message.as_bytes());
 
// Submit the request directly
let (request_id, expires_at) = client.submit_onchain(request).await?; 

Offchain

To submit a request offchain, we use:









// Create a request using new_request
let request = client.new_request().with_program(ECHO_ELF).with_stdin(echo_message.as_bytes());
 
// Submit the request directly
let (request_id, expires_at) = client.submit_offchain(request).await?; 

Offer

The Offer specifies how much the requestor will pay for a proof, by setting the auction price, timing, stake requirements, and expiration.

The Client helps you build requests and set these parameters. Within the client, the OfferLayer creates the offer. It contains a set of defaults, and logic to assign a price to your request.

There are two ways to configure offer parameters:

  1. Using client_builder.config_offer_layer to configure the offer building logic.
  2. Using request.with_offer to override parameters for a specific request. This gives you direct control over the offer.

When to Use Each Approach

  • Use config_offer_layer when:

    • You want to configure cycle-based pricing that applies to all requests
    • You need to adjust gas estimates or other calculation parameters
    • You want consistent pricing logic across multiple requests
  • Use with_offer when:

    • You need to override the automatic calculations for a specific request
    • You want to set exact prices rather than using cycle-based and gas-price calculations
    • You have special requirements for a particular proof request

Per-Request Configuration with with_offer

Use with_offer when you want to override specific pricing parameters for an individual request:








// Create a request using new_request
let request = client.new_request()
  .with_program(program)
  .with_stdin(input)
  .with_offer(
    OfferParams::builder()
      // 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.
      .min_price(parse_ether("0.001")?)
      .max_price(parse_ether("0.002")?)
      // 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 lock timeout, the
      // prover can be slashed.
      .timeout(1000)
      .lock_timeout(500)
      .ramp_up_period(100)
  );
 
// Submit the request directly
let (request_id, expires_at) = client.submit_onchain(request).await?;

Client-Level Configuration with config_offer_layer

Use config_offer_layer when you want to adjust how the SDK calculates offer parameters based on cycle count and gas prices. This is particularly useful when you want to use cycle-based pricing:


















// Configure the offer layer logic when building the client
let client = Client::builder()
  .with_rpc_url(args.rpc_url)
  .with_private_key(args.private_key)
  .with_storage_provider(Some(storage_provider_from_env()?))
  .config_offer_layer(|config| config
    // Set the price per cycle for automatic pricing calculations
    .max_price_per_cycle(parse_units("0.1", "gwei").unwrap())
    .min_price_per_cycle(parse_units("0.01", "gwei").unwrap())
    // Configure default timeouts and auction parameters
    .ramp_up_period(36)
    .lock_timeout(120)
    .timeout(300)
  )
  .build()
  .await?;

With this configuration, the SDK will execute the request to estimate cycles and calculate appropriate prices.