Skip to main content

Default: Merkle Inclusion Proofs

By default, Boundless delivers proofs on-chain as merkle inclusion proofs:
  1. Proofs are batched together are aggregated into a single Groth16 proof.
  2. The aggregated proof is verified once on-chain
  3. Individual proofs are verified through cheap merkle inclusion proofs into this root
This design is what makes Boundless cost-effective for on-chain verification.

Options: Requesting a Specific Proof Type

While Merkle inclusion proofs are efficient for on-chain verification, there may be cases where you need to access the underlying proof instead of a merkle inclusion proof. For example:
  1. Cross-chain verification where you need to verify the proof on a different chain.
  2. Integration with other systems that expect a specific proof type.
  3. Custom verification logic that requires the full proof.

Request a Groth16 Proof

Boundless supports requesting a raw Groth16 proof instead of a merkle inclusion proof. You can specify this in your proof request by setting the proof_type to ProofType::Groth16:
let request = client.new_request()
    .with_program(program)
    .with_stdin(input)
    .with_groth16_proof()  // Request raw Groth16 proof
    .with_offer(
        OfferParams::builder()
            .min_price(parse_ether("0.001")?)
            .max_price(parse_ether("0.002")?)
            .timeout(1000)
            .lock_timeout(1000)
    );

Request a Blake3 Groth16 Proof

Blake3 Groth16 proofs are only supported with the ClaimDigestMatch predicate, meaning that you should only use this if you do not require the journal to be delivered on-chain. Blake3 Groth16 proofs also require the journal to be of size 32 bytes.
Boundless supports requesting a Blake3 Groth16 proof. This proof type allows for proofs to be verified in environments where SHA2 hashing is impossible or expensive (e.g. BitVM). You can specify this in your proof request by setting the proof_type to ProofType::Blake3Groth16:
let request = client
    .new_request()
    .with_program(program)
    .with_stdin(input)
    .with_blake3_groth16_proof()  // Request Blake3 Groth16 proof
    .with_offer(
        OfferParams::builder()
            .min_price(parse_ether("0.001")?)
            .max_price(parse_ether("0.002")?)
            .timeout(1000)
            .lock_timeout(1000)
    );

Considerations

When choosing between proof types, consider:
  1. Gas Costs
    • Merkle inclusion proofs are much cheaper to verify on-chain
    • Raw Groth16 proofs require full SNARK verification each time. This will increase the price of the proof
  2. Use Case Requirements
    • If you only need on-chain verification, use the default merkle inclusion proof
    • If you need cross-chain verification or raw proof data, use Groth16
    • If you need to compose the proof by verifying it within another zkVM guest program, use Groth16
    • If you don’t need the journal on-chain, consider using ClaimDigestMatch to save gas (see Journal Delivery)
    • If your journal size exceeds 10KB, use ClaimDigestMatch and design your application to store journals off-chain (see Journal Size Limits)

Journal Delivery Onchain

When a proof request is fulfilled, the journal can optionally be delivered on-chain. This is controlled by the predicate type you specify in your Requirement:
  • DigestMatch / PrefixMatch require the journal to be delivered on-chain when the request is fulfilled.
  • ClaimDigestMatch does not require journal delivery. Only the claim digest is verified on-chain.
If your application doesn’t need the journal on-chain, using ClaimDigestMatch can lead to lower prices since provers won’t need to submit potentially large journal data.

Journal Size Limits

To prevent griefing attacks where requestors force provers to post expensive amounts of calldata on-chain, there is a 10KB limit on journal size for on-chain delivery. Provers will ignore requests that require journals larger than 10KB to be posted on-chain. If your journal exceeds 10KB, use ClaimDigestMatch and design your application to store journals off-chain (e.g. via IPFS, a blob storage service, or your own backend).

Example: Proof Composition using Proof Types

In the Proof Composition example, we demonstrate how to compose a proof from multiple proofs. Composing a proof requires us to verify a previously generated Groth16 proof within the zkVM guest program. This requires us to request a raw Groth16 proof from the Boundless Market. In the composition example, we first request a raw Groth16 proof from the Boundless Market using the ECHO guest program.
let request = client.new_request()
    .with_program(program)
    .with_stdin(input)
    .with_groth16_proof()  // Request raw Groth16 proof
    .with_offer(
        OfferParams::builder()
            .min_price(parse_ether("0.001")?)
            .max_price(parse_ether("0.002")?)
            .timeout(1000)
            .lock_timeout(1000)
    );
We then provide the Groth16 proof as input to the IDENTITY zkVM guest program, and verify the proof.
use risc0_zkvm::guest::env;
use risc0_zkvm::{Digest, Receipt};

fn main() {
    let (image_id, receipt): (Digest, Receipt) = env::read();
    let claim = receipt.claim().unwrap();
    receipt.verify(image_id).unwrap();
    ....
}