boundless_cli/
lib.rs

1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The Boundless CLI is a command-line interface for interacting with the Boundless Market API.
16
17#![deny(missing_docs)]
18
19use alloy::{
20    primitives::Address,
21    sol_types::{SolStruct, SolValue},
22};
23use anyhow::{bail, Context, Result};
24use bonsai_sdk::non_blocking::Client as BonsaiClient;
25use boundless_assessor::{AssessorInput, Fulfillment};
26use chrono::{DateTime, Local};
27use risc0_aggregation::{
28    merkle_path, GuestState, SetInclusionReceipt, SetInclusionReceiptVerifierParameters,
29};
30use risc0_ethereum_contracts::encode_seal;
31use risc0_zkvm::{
32    compute_image_id, default_prover, is_dev_mode,
33    sha::{Digest, Digestible},
34    ExecutorEnv, ProverOpts, Receipt, ReceiptClaim,
35};
36use url::Url;
37
38use boundless_market::{
39    contracts::{
40        AssessorJournal, AssessorReceipt, EIP712DomainSaltless,
41        Fulfillment as BoundlessFulfillment, InputType,
42    },
43    input::{GuestEnv, InputBuilder},
44    order_stream_client::Order,
45    selector::{is_groth16_selector, SupportedSelectors},
46};
47
48alloy::sol!(
49    #[sol(all_derives)]
50    /// The fulfillment of an order.
51    struct OrderFulfilled {
52        /// The root of the set.
53        bytes32 root;
54        /// The seal of the root.
55        bytes seal;
56        /// The fulfillments of the order.
57        BoundlessFulfillment[] fills;
58        /// The fulfillment of the assessor.
59        AssessorReceipt assessorReceipt;
60    }
61);
62
63impl OrderFulfilled {
64    /// Creates a new [OrderFulfilled],
65    pub fn new(
66        fill: BoundlessFulfillment,
67        root_receipt: Receipt,
68        assessor_receipt: AssessorReceipt,
69    ) -> Result<Self> {
70        let state = GuestState::decode(&root_receipt.journal.bytes)?;
71        let root = state.mmr.finalized_root().context("failed to get finalized root")?;
72
73        let root_seal = encode_seal(&root_receipt)?;
74
75        Ok(OrderFulfilled {
76            root: <[u8; 32]>::from(root).into(),
77            seal: root_seal.into(),
78            fills: vec![fill],
79            assessorReceipt: assessor_receipt,
80        })
81    }
82}
83
84/// Converts a timestamp to a [DateTime] in the local timezone.
85pub fn convert_timestamp(timestamp: u64) -> DateTime<Local> {
86    let t = DateTime::from_timestamp(timestamp as i64, 0).expect("invalid timestamp");
87    t.with_timezone(&Local)
88}
89
90/// Fetches the content of a URL.
91/// Supported URL schemes are `http`, `https`, and `file`.
92pub async fn fetch_url(url_str: &str) -> Result<Vec<u8>> {
93    tracing::debug!("Fetching URL: {}", url_str);
94    let url = Url::parse(url_str)?;
95
96    match url.scheme() {
97        "http" | "https" => fetch_http(&url).await,
98        "file" => fetch_file(&url).await,
99        _ => bail!("unsupported URL scheme: {}", url.scheme()),
100    }
101}
102
103async fn fetch_http(url: &Url) -> Result<Vec<u8>> {
104    let response = reqwest::get(url.as_str()).await?;
105    let status = response.status();
106    if !status.is_success() {
107        bail!("HTTP request failed with status: {}", status);
108    }
109
110    Ok(response.bytes().await?.to_vec())
111}
112
113async fn fetch_file(url: &Url) -> Result<Vec<u8>> {
114    let path = std::path::Path::new(url.path());
115    let data = tokio::fs::read(path).await?;
116    Ok(data)
117}
118
119/// The default prover implementation.
120/// This [DefaultProver] uses the default zkVM prover.
121/// The selection of the zkVM prover is based on environment variables.
122///
123/// The `RISC0_PROVER` environment variable, if specified, will select the
124/// following [Prover] implementation:
125/// * `bonsai`: [BonsaiProver] to prove on Bonsai.
126/// * `local`: LocalProver to prove locally in-process. Note: this
127///   requires the `prove` feature flag.
128/// * `ipc`: [ExternalProver] to prove using an `r0vm` sub-process. Note: `r0vm`
129///   must be installed. To specify the path to `r0vm`, use `RISC0_SERVER_PATH`.
130///
131/// If `RISC0_PROVER` is not specified, the following rules are used to select a
132/// [Prover]:
133/// * [BonsaiProver] if the `BONSAI_API_URL` and `BONSAI_API_KEY` environment
134///   variables are set unless `RISC0_DEV_MODE` is enabled.
135/// * LocalProver if the `prove` feature flag is enabled.
136/// * [ExternalProver] otherwise.
137pub struct DefaultProver {
138    set_builder_elf: Vec<u8>,
139    set_builder_image_id: Digest,
140    assessor_elf: Vec<u8>,
141    address: Address,
142    domain: EIP712DomainSaltless,
143    supported_selectors: SupportedSelectors,
144}
145
146impl DefaultProver {
147    /// Creates a new [DefaultProver].
148    pub fn new(
149        set_builder_elf: Vec<u8>,
150        assessor_elf: Vec<u8>,
151        address: Address,
152        domain: EIP712DomainSaltless,
153    ) -> Result<Self> {
154        let set_builder_image_id = compute_image_id(&set_builder_elf)?;
155        let supported_selectors =
156            SupportedSelectors::default().with_set_builder_image_id(set_builder_image_id);
157        Ok(Self {
158            set_builder_elf,
159            set_builder_image_id,
160            assessor_elf,
161            address,
162            domain,
163            supported_selectors,
164        })
165    }
166
167    // Proves the given [elf] with the given [input] and [assumptions].
168    // The [opts] parameter specifies the prover options.
169    pub(crate) async fn prove(
170        &self,
171        elf: Vec<u8>,
172        input: Vec<u8>,
173        assumptions: Vec<Receipt>,
174        opts: ProverOpts,
175    ) -> Result<Receipt> {
176        let receipt = tokio::task::spawn_blocking(move || {
177            let mut env = ExecutorEnv::builder();
178            env.write_slice(&input);
179            for assumption_receipt in assumptions.iter() {
180                env.add_assumption(assumption_receipt.clone());
181            }
182            let env = env.build()?;
183
184            default_prover().prove_with_opts(env, &elf, &opts)
185        })
186        .await??
187        .receipt;
188        Ok(receipt)
189    }
190
191    pub(crate) async fn compress(&self, succinct_receipt: &Receipt) -> Result<Receipt> {
192        let prover = default_prover();
193        if prover.get_name() == "bonsai" {
194            return compress_with_bonsai(succinct_receipt).await;
195        }
196        if is_dev_mode() {
197            return Ok(succinct_receipt.clone());
198        }
199
200        let receipt = succinct_receipt.clone();
201        tokio::task::spawn_blocking(move || {
202            default_prover().compress(&ProverOpts::groth16(), &receipt)
203        })
204        .await?
205    }
206
207    // Finalizes the set builder.
208    pub(crate) async fn finalize(
209        &self,
210        claims: Vec<ReceiptClaim>,
211        assumptions: Vec<Receipt>,
212    ) -> Result<Receipt> {
213        let input = GuestState::initial(self.set_builder_image_id)
214            .into_input(claims, true)
215            .context("Failed to build set builder input")?;
216        let encoded_input = bytemuck::pod_collect_to_vec(&risc0_zkvm::serde::to_vec(&input)?);
217
218        self.prove(self.set_builder_elf.clone(), encoded_input, assumptions, ProverOpts::groth16())
219            .await
220    }
221
222    // Proves the assessor.
223    pub(crate) async fn assessor(
224        &self,
225        fills: Vec<Fulfillment>,
226        receipts: Vec<Receipt>,
227    ) -> Result<Receipt> {
228        let assessor_input =
229            AssessorInput { domain: self.domain.clone(), fills, prover_address: self.address };
230
231        let stdin = InputBuilder::new().write_frame(&assessor_input.encode()).stdin;
232
233        self.prove(self.assessor_elf.clone(), stdin, receipts, ProverOpts::succinct()).await
234    }
235
236    /// Fulfills an order as a singleton, returning the relevant data:
237    /// * The [Fulfillment] of the order.
238    /// * The [Receipt] of the root set.
239    /// * The [SetInclusionReceipt] of the order.
240    /// * The [SetInclusionReceipt] of the assessor.
241    pub async fn fulfill(
242        &self,
243        order: Order,
244    ) -> Result<(BoundlessFulfillment, Receipt, AssessorReceipt)> {
245        let request = order.request.clone();
246        let order_elf = fetch_url(&request.imageUrl).await?;
247        let order_input: Vec<u8> = match request.input.inputType {
248            InputType::Inline => GuestEnv::decode(&request.input.data)?.stdin,
249            InputType::Url => {
250                GuestEnv::decode(
251                    &fetch_url(
252                        std::str::from_utf8(&request.input.data)
253                            .context("input url is not utf8")?,
254                    )
255                    .await?,
256                )?
257                .stdin
258            }
259            _ => bail!("Unsupported input type"),
260        };
261
262        let selector = request.requirements.selector;
263        if !self.supported_selectors.is_supported(selector) {
264            bail!("Unsupported selector {}", request.requirements.selector);
265        };
266
267        let order_receipt = self
268            .prove(order_elf.clone(), order_input.clone(), vec![], ProverOpts::succinct())
269            .await?;
270
271        let order_journal = order_receipt.journal.bytes.clone();
272        let order_image_id = compute_image_id(&order_elf)?;
273
274        let fill = Fulfillment {
275            request: order.request.clone(),
276            signature: order.signature.into(),
277            journal: order_journal.clone(),
278        };
279
280        let assessor_receipt = self.assessor(vec![fill], vec![order_receipt.clone()]).await?;
281        let assessor_journal = assessor_receipt.journal.bytes.clone();
282        let assessor_image_id = compute_image_id(&self.assessor_elf)?;
283
284        let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
285        let order_claim_digest = order_claim.digest();
286        let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal.clone());
287        let assessor_claim_digest = assessor_claim.digest();
288        let assessor_receipt_journal: AssessorJournal =
289            AssessorJournal::abi_decode(&assessor_journal, true)?;
290        let root_receipt = self
291            .finalize(
292                vec![order_claim.clone(), assessor_claim.clone()],
293                vec![order_receipt.clone(), assessor_receipt],
294            )
295            .await?;
296
297        let order_path = merkle_path(&[order_claim_digest, assessor_claim_digest], 0);
298        let assessor_path = merkle_path(&[order_claim_digest, assessor_claim_digest], 1);
299
300        let verifier_parameters =
301            SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
302
303        let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
304            order_claim,
305            order_path,
306            verifier_parameters.digest(),
307        );
308        let order_seal = if is_groth16_selector(selector) {
309            let receipt = self.compress(&order_receipt).await?;
310            encode_seal(&receipt)?
311        } else {
312            order_inclusion_receipt.abi_encode_seal()?
313        };
314
315        let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
316            assessor_claim,
317            assessor_path,
318            verifier_parameters.digest(),
319        );
320
321        let fulfillment = BoundlessFulfillment {
322            id: request.id,
323            requestDigest: order.request.eip712_signing_hash(&self.domain.alloy_struct()),
324            imageId: request.requirements.imageId,
325            journal: order_journal.into(),
326            seal: order_seal.into(),
327        };
328
329        let assessor_receipt = AssessorReceipt {
330            seal: assessor_inclusion_receipt.abi_encode_seal()?.into(),
331            prover: self.address,
332            selectors: assessor_receipt_journal.selectors,
333            callbacks: assessor_receipt_journal.callbacks,
334        };
335
336        Ok((fulfillment, root_receipt, assessor_receipt))
337    }
338}
339
340async fn compress_with_bonsai(succinct_receipt: &Receipt) -> Result<Receipt> {
341    let client = BonsaiClient::from_env(risc0_zkvm::VERSION)?;
342    let encoded_receipt = bincode::serialize(succinct_receipt)?;
343    let receipt_id = client.upload_receipt(encoded_receipt).await?;
344    let snark_id = client.create_snark(receipt_id).await?;
345    loop {
346        let status = snark_id.status(&client).await?;
347        match status.status.as_ref() {
348            "RUNNING" => {
349                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
350                continue;
351            }
352            "SUCCEEDED" => {
353                let receipt_buf = client.download(&status.output.unwrap()).await?;
354                let snark_receipt: Receipt = bincode::deserialize(&receipt_buf)?;
355                return Ok(snark_receipt);
356            }
357            _ => {
358                let err_msg = status.error_msg.unwrap_or_default();
359                return Err(anyhow::anyhow!("snark proving failed: {err_msg}"));
360            }
361        }
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use alloy::{
369        primitives::{FixedBytes, PrimitiveSignature},
370        signers::local::PrivateKeySigner,
371    };
372    use boundless_market::contracts::{
373        eip712_domain, Input, Offer, Predicate, ProofRequest, RequestId, Requirements,
374        UNSPECIFIED_SELECTOR,
375    };
376    use guest_assessor::ASSESSOR_GUEST_ELF;
377    use guest_set_builder::SET_BUILDER_ELF;
378    use guest_util::{ECHO_ID, ECHO_PATH};
379    use risc0_ethereum_contracts::selector::Selector;
380
381    async fn setup_proving_request_and_signature(
382        signer: &PrivateKeySigner,
383        selector: Option<Selector>,
384    ) -> (ProofRequest, PrimitiveSignature) {
385        let request = ProofRequest::new(
386            RequestId::new(signer.address(), 0),
387            Requirements::new(Digest::from(ECHO_ID), Predicate::prefix_match(vec![1]))
388                .with_selector(match selector {
389                    Some(selector) => FixedBytes::from(selector as u32),
390                    None => UNSPECIFIED_SELECTOR,
391                }),
392            format!("file://{ECHO_PATH}"),
393            Input::builder().write_slice(&[1, 2, 3, 4]).build_inline().unwrap(),
394            Offer::default(),
395        );
396
397        let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
398        (request, signature)
399    }
400
401    #[tokio::test]
402    #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
403    async fn test_fulfill_with_selector() {
404        let signer = PrivateKeySigner::random();
405        let (request, signature) =
406            setup_proving_request_and_signature(&signer, Some(Selector::Groth16V2_0)).await;
407
408        let domain = eip712_domain(Address::ZERO, 1);
409        let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
410        let prover = DefaultProver::new(
411            SET_BUILDER_ELF.to_vec(),
412            ASSESSOR_GUEST_ELF.to_vec(),
413            Address::ZERO,
414            domain,
415        )
416        .expect("failed to create prover");
417
418        let order = Order { request, request_digest, signature };
419        prover.fulfill(order.clone()).await.unwrap();
420    }
421
422    #[tokio::test]
423    #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
424    async fn test_fulfill() {
425        let signer = PrivateKeySigner::random();
426        let (request, signature) = setup_proving_request_and_signature(&signer, None).await;
427
428        let domain = eip712_domain(Address::ZERO, 1);
429        let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
430        let prover = DefaultProver::new(
431            SET_BUILDER_ELF.to_vec(),
432            ASSESSOR_GUEST_ELF.to_vec(),
433            Address::ZERO,
434            domain,
435        )
436        .expect("failed to create prover");
437
438        let order = Order { request, request_digest, signature };
439        prover.fulfill(order.clone()).await.unwrap();
440    }
441}