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        fills: Vec<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,
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_program: Vec<u8>,
139    set_builder_image_id: Digest,
140    assessor_program: 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_program: Vec<u8>,
150        assessor_program: Vec<u8>,
151        address: Address,
152        domain: EIP712DomainSaltless,
153    ) -> Result<Self> {
154        let set_builder_image_id = compute_image_id(&set_builder_program)?;
155        let supported_selectors =
156            SupportedSelectors::default().with_set_builder_image_id(set_builder_image_id);
157        Ok(Self {
158            set_builder_program,
159            set_builder_image_id,
160            assessor_program,
161            address,
162            domain,
163            supported_selectors,
164        })
165    }
166
167    // Proves the given [program] with the given [input] and [assumptions].
168    // The [opts] parameter specifies the prover options.
169    pub(crate) async fn prove(
170        &self,
171        program: 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, &program, &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(
219            self.set_builder_program.clone(),
220            encoded_input,
221            assumptions,
222            ProverOpts::groth16(),
223        )
224        .await
225    }
226
227    // Proves the assessor.
228    pub(crate) async fn assessor(
229        &self,
230        fills: Vec<Fulfillment>,
231        receipts: Vec<Receipt>,
232    ) -> Result<Receipt> {
233        let assessor_input =
234            AssessorInput { domain: self.domain.clone(), fills, prover_address: self.address };
235
236        let stdin = InputBuilder::new().write_frame(&assessor_input.encode()).stdin;
237
238        self.prove(self.assessor_program.clone(), stdin, receipts, ProverOpts::succinct()).await
239    }
240
241    /// Fulfills a list of orders, returning the relevant data:
242    /// * A list of [Fulfillment] of the orders.
243    /// * The [Receipt] of the root set.
244    /// * The [SetInclusionReceipt] of the assessor.
245    pub async fn fulfill(
246        &self,
247        orders: &[Order],
248    ) -> Result<(Vec<BoundlessFulfillment>, Receipt, AssessorReceipt)> {
249        let orders_jobs = orders.iter().map(|order| async {
250            let request = order.request.clone();
251            let order_program = fetch_url(&request.imageUrl).await?;
252            let order_input: Vec<u8> = match request.input.inputType {
253                InputType::Inline => GuestEnv::decode(&request.input.data)?.stdin,
254                InputType::Url => {
255                    GuestEnv::decode(
256                        &fetch_url(
257                            std::str::from_utf8(&request.input.data)
258                                .context("input url is not utf8")?,
259                        )
260                        .await?,
261                    )?
262                    .stdin
263                }
264                _ => bail!("Unsupported input type"),
265            };
266
267            let selector = request.requirements.selector;
268            if !self.supported_selectors.is_supported(selector) {
269                bail!("Unsupported selector {}", request.requirements.selector);
270            };
271
272            let order_receipt = self
273                .prove(order_program.clone(), order_input.clone(), vec![], ProverOpts::succinct())
274                .await?;
275
276            let order_journal = order_receipt.journal.bytes.clone();
277            let order_image_id = compute_image_id(&order_program)?;
278            let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
279            let order_claim_digest = order_claim.digest();
280
281            let fill = Fulfillment {
282                request: order.request.clone(),
283                signature: order.signature.into(),
284                journal: order_journal.clone(),
285            };
286
287            Ok::<_, anyhow::Error>((order_receipt, order_claim, order_claim_digest, fill))
288        });
289
290        let results = futures::future::join_all(orders_jobs).await;
291        let mut receipts = Vec::new();
292        let mut claims = Vec::new();
293        let mut claim_digests = Vec::new();
294        let mut fills = Vec::new();
295
296        for (i, result) in results.into_iter().enumerate() {
297            if let Err(e) = result {
298                tracing::warn!("Failed to prove request 0x{:x}: {}", orders[i].request.id, e);
299                continue;
300            }
301            let (receipt, claim, claim_digest, fill) = result?;
302            receipts.push(receipt);
303            claims.push(claim);
304            claim_digests.push(claim_digest);
305            fills.push(fill);
306        }
307
308        let assessor_receipt = self.assessor(fills.clone(), receipts.clone()).await?;
309        let assessor_journal = assessor_receipt.journal.bytes.clone();
310        let assessor_image_id = compute_image_id(&self.assessor_program)?;
311        let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal.clone());
312        let assessor_receipt_journal: AssessorJournal =
313            AssessorJournal::abi_decode(&assessor_journal)?;
314
315        receipts.push(assessor_receipt);
316        claims.push(assessor_claim.clone());
317        claim_digests.push(assessor_claim.digest());
318
319        let root_receipt = self.finalize(claims.clone(), receipts.clone()).await?;
320
321        let verifier_parameters =
322            SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
323
324        let mut boundless_fills = Vec::new();
325
326        for i in 0..fills.len() {
327            let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
328                claims[i].clone(),
329                merkle_path(&claim_digests, i),
330                verifier_parameters.digest(),
331            );
332            let order = &orders[i];
333            let order_seal = if is_groth16_selector(order.request.requirements.selector) {
334                let receipt = self.compress(&receipts[i]).await?;
335                encode_seal(&receipt)?
336            } else {
337                order_inclusion_receipt.abi_encode_seal()?
338            };
339
340            let fulfillment = BoundlessFulfillment {
341                id: order.request.id,
342                requestDigest: order.request.eip712_signing_hash(&self.domain.alloy_struct()),
343                imageId: order.request.requirements.imageId,
344                journal: fills[i].journal.clone().into(),
345                seal: order_seal.into(),
346            };
347
348            boundless_fills.push(fulfillment);
349        }
350
351        let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
352            assessor_claim,
353            merkle_path(&claim_digests, claim_digests.len() - 1),
354            verifier_parameters.digest(),
355        );
356
357        let assessor_receipt = AssessorReceipt {
358            seal: assessor_inclusion_receipt.abi_encode_seal()?.into(),
359            prover: self.address,
360            selectors: assessor_receipt_journal.selectors,
361            callbacks: assessor_receipt_journal.callbacks,
362        };
363
364        Ok((boundless_fills, root_receipt, assessor_receipt))
365    }
366}
367
368async fn compress_with_bonsai(succinct_receipt: &Receipt) -> Result<Receipt> {
369    let client = BonsaiClient::from_env(risc0_zkvm::VERSION)?;
370    let encoded_receipt = bincode::serialize(succinct_receipt)?;
371    let receipt_id = client.upload_receipt(encoded_receipt).await?;
372    let snark_id = client.create_snark(receipt_id).await?;
373    loop {
374        let status = snark_id.status(&client).await?;
375        match status.status.as_ref() {
376            "RUNNING" => {
377                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
378                continue;
379            }
380            "SUCCEEDED" => {
381                let receipt_buf = client.download(&status.output.unwrap()).await?;
382                let snark_receipt: Receipt = bincode::deserialize(&receipt_buf)?;
383                return Ok(snark_receipt);
384            }
385            _ => {
386                let err_msg = status.error_msg.unwrap_or_default();
387                return Err(anyhow::anyhow!("snark proving failed: {err_msg}"));
388            }
389        }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use alloy::{
397        primitives::{FixedBytes, Signature},
398        signers::local::PrivateKeySigner,
399    };
400    use boundless_market::contracts::{
401        eip712_domain, Input, Offer, Predicate, ProofRequest, RequestId, Requirements,
402        UNSPECIFIED_SELECTOR,
403    };
404    use boundless_market_test_utils::{ASSESSOR_GUEST_ELF, ECHO_ID, ECHO_PATH, SET_BUILDER_ELF};
405    use risc0_ethereum_contracts::selector::Selector;
406
407    async fn setup_proving_request_and_signature(
408        signer: &PrivateKeySigner,
409        selector: Option<Selector>,
410    ) -> (ProofRequest, Signature) {
411        let request = ProofRequest::new(
412            RequestId::new(signer.address(), 0),
413            Requirements::new(Digest::from(ECHO_ID), Predicate::prefix_match(vec![1]))
414                .with_selector(match selector {
415                    Some(selector) => FixedBytes::from(selector as u32),
416                    None => UNSPECIFIED_SELECTOR,
417                }),
418            format!("file://{ECHO_PATH}"),
419            Input::builder().write_slice(&[1, 2, 3, 4]).build_inline().unwrap(),
420            Offer::default(),
421        );
422
423        let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
424        (request, signature)
425    }
426
427    #[tokio::test]
428    #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
429    async fn test_fulfill_with_selector() {
430        let signer = PrivateKeySigner::random();
431        let (request, signature) =
432            setup_proving_request_and_signature(&signer, Some(Selector::Groth16V2_0)).await;
433
434        let domain = eip712_domain(Address::ZERO, 1);
435        let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
436        let prover = DefaultProver::new(
437            SET_BUILDER_ELF.to_vec(),
438            ASSESSOR_GUEST_ELF.to_vec(),
439            Address::ZERO,
440            domain,
441        )
442        .expect("failed to create prover");
443
444        let order = Order { request, request_digest, signature };
445        prover.fulfill(&[order.clone()]).await.unwrap();
446    }
447
448    #[tokio::test]
449    #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
450    async fn test_fulfill() {
451        let signer = PrivateKeySigner::random();
452        let (request, signature) = setup_proving_request_and_signature(&signer, None).await;
453
454        let domain = eip712_domain(Address::ZERO, 1);
455        let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
456        let prover = DefaultProver::new(
457            SET_BUILDER_ELF.to_vec(),
458            ASSESSOR_GUEST_ELF.to_vec(),
459            Address::ZERO,
460            domain,
461        )
462        .expect("failed to create prover");
463
464        let order = Order { request, request_digest, signature };
465        prover.fulfill(&[order.clone()]).await.unwrap();
466    }
467}