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::{primitives::Address, sol_types::SolStruct};
20use anyhow::{bail, Context, Result};
21use boundless_assessor::{AssessorInput, Fulfillment};
22use risc0_aggregation::{
23    merkle_path, GuestState, SetInclusionReceipt, SetInclusionReceiptVerifierParameters,
24};
25use risc0_ethereum_contracts::encode_seal;
26use risc0_zkvm::{
27    compute_image_id, default_prover,
28    sha::{Digest, Digestible},
29    ExecutorEnv, ProverOpts, Receipt, ReceiptClaim,
30};
31use url::Url;
32
33use boundless_market::{
34    contracts::{EIP721DomainSaltless, Fulfillment as BoundlessFulfillment, InputType},
35    input::GuestEnv,
36    order_stream_client::Order,
37};
38
39alloy::sol!(
40    #[sol(all_derives)]
41    /// The fulfillment of an order.
42    struct OrderFulfilled {
43        /// The root of the set.
44        bytes32 root;
45        /// The seal of the root.
46        bytes seal;
47        /// The fulfillments of the order.
48        BoundlessFulfillment[] fills;
49        /// The seal of the assessor.
50        bytes assessorSeal;
51        /// The prover address.
52        address prover;
53    }
54);
55
56impl OrderFulfilled {
57    /// Creates a new [OrderFulfilled],
58    pub fn new(
59        fill: BoundlessFulfillment,
60        root_receipt: Receipt,
61        assessor_receipt: SetInclusionReceipt<ReceiptClaim>,
62        prover: Address,
63    ) -> Result<Self> {
64        let state = GuestState::decode(&root_receipt.journal.bytes)?;
65        let root = state.mmr.finalized_root().context("failed to get finalized root")?;
66
67        let root_seal = encode_seal(&root_receipt)?;
68        let assessor_seal = assessor_receipt.abi_encode_seal()?;
69
70        Ok(OrderFulfilled {
71            root: <[u8; 32]>::from(root).into(),
72            seal: root_seal.into(),
73            fills: vec![fill],
74            assessorSeal: assessor_seal.into(),
75            prover,
76        })
77    }
78}
79
80/// Fetches the content of a URL.
81/// Supported URL schemes are `http`, `https`, and `file`.
82pub async fn fetch_url(url_str: &str) -> Result<Vec<u8>> {
83    tracing::debug!("Fetching URL: {}", url_str);
84    let url = Url::parse(url_str)?;
85
86    match url.scheme() {
87        "http" | "https" => fetch_http(&url).await,
88        "file" => fetch_file(&url).await,
89        _ => bail!("unsupported URL scheme: {}", url.scheme()),
90    }
91}
92
93async fn fetch_http(url: &Url) -> Result<Vec<u8>> {
94    let response = reqwest::get(url.as_str()).await?;
95    let status = response.status();
96    if !status.is_success() {
97        bail!("HTTP request failed with status: {}", status);
98    }
99
100    Ok(response.bytes().await?.to_vec())
101}
102
103async fn fetch_file(url: &Url) -> Result<Vec<u8>> {
104    let path = std::path::Path::new(url.path());
105    let data = tokio::fs::read(path).await?;
106    Ok(data)
107}
108
109/// The default prover implementation.
110/// This [DefaultProver] uses the default zkVM prover.
111/// The selection of the zkVM prover is based on environment variables.
112///
113/// The `RISC0_PROVER` environment variable, if specified, will select the
114/// following [Prover] implementation:
115/// * `bonsai`: [BonsaiProver] to prove on Bonsai.
116/// * `local`: LocalProver to prove locally in-process. Note: this
117///   requires the `prove` feature flag.
118/// * `ipc`: [ExternalProver] to prove using an `r0vm` sub-process. Note: `r0vm`
119///   must be installed. To specify the path to `r0vm`, use `RISC0_SERVER_PATH`.
120///
121/// If `RISC0_PROVER` is not specified, the following rules are used to select a
122/// [Prover]:
123/// * [BonsaiProver] if the `BONSAI_API_URL` and `BONSAI_API_KEY` environment
124///   variables are set unless `RISC0_DEV_MODE` is enabled.
125/// * LocalProver if the `prove` feature flag is enabled.
126/// * [ExternalProver] otherwise.
127pub struct DefaultProver {
128    set_builder_elf: Vec<u8>,
129    set_builder_image_id: Digest,
130    assessor_elf: Vec<u8>,
131    address: Address,
132    domain: EIP721DomainSaltless,
133}
134
135impl DefaultProver {
136    /// Creates a new [DefaultProver].
137    pub fn new(
138        set_builder_elf: Vec<u8>,
139        assessor_elf: Vec<u8>,
140        address: Address,
141        domain: EIP721DomainSaltless,
142    ) -> Result<Self> {
143        let set_builder_image_id = compute_image_id(&set_builder_elf)?;
144        Ok(Self { set_builder_elf, set_builder_image_id, assessor_elf, address, domain })
145    }
146
147    // Proves the given [elf] with the given [input] and [assumptions].
148    // The [opts] parameter specifies the prover options.
149    pub(crate) async fn prove(
150        &self,
151        elf: Vec<u8>,
152        input: Vec<u8>,
153        assumptions: Vec<Receipt>,
154        opts: ProverOpts,
155    ) -> Result<Receipt> {
156        let receipt = tokio::task::spawn_blocking(move || {
157            let mut env = ExecutorEnv::builder();
158            env.write_slice(&input);
159            for assumption_receipt in assumptions.iter() {
160                env.add_assumption(assumption_receipt.clone());
161            }
162            let env = env.build()?;
163            default_prover().prove_with_opts(env, &elf, &opts)
164        })
165        .await??
166        .receipt;
167        Ok(receipt)
168    }
169
170    // Finalizes the set builder.
171    pub(crate) async fn finalize(
172        &self,
173        claims: Vec<ReceiptClaim>,
174        assumptions: Vec<Receipt>,
175    ) -> Result<Receipt> {
176        let input = GuestState::initial(self.set_builder_image_id)
177            .into_input(claims, true)
178            .context("Failed to build set builder input")?;
179        let encoded_input = bytemuck::pod_collect_to_vec(&risc0_zkvm::serde::to_vec(&input)?);
180
181        self.prove(self.set_builder_elf.clone(), encoded_input, assumptions, ProverOpts::succinct())
182            .await
183    }
184
185    // Proves the assessor.
186    pub(crate) async fn assessor(
187        &self,
188        fills: Vec<Fulfillment>,
189        receipts: Vec<Receipt>,
190    ) -> Result<Receipt> {
191        let assessor_input =
192            AssessorInput { domain: self.domain.clone(), fills, prover_address: self.address };
193        self.prove(
194            self.assessor_elf.clone(),
195            assessor_input.to_vec(),
196            receipts,
197            ProverOpts::succinct(),
198        )
199        .await
200    }
201
202    /// Fulfills an order as a singleton, returning the relevant data:
203    /// * The [Fulfillment] of the order.
204    /// * The [Receipt] of the root set.
205    /// * The [SetInclusionReceipt] of the order.
206    /// * The [SetInclusionReceipt] of the assessor.
207    pub async fn fulfill(
208        &self,
209        order: Order,
210        require_payment: bool,
211    ) -> Result<(
212        BoundlessFulfillment,
213        Receipt,
214        SetInclusionReceipt<ReceiptClaim>,
215        SetInclusionReceipt<ReceiptClaim>,
216    )> {
217        let request = order.request.clone();
218        let order_elf = fetch_url(&request.imageUrl).await?;
219        let order_input: Vec<u8> = match request.input.inputType {
220            InputType::Inline => GuestEnv::decode(&request.input.data)?.stdin,
221            InputType::Url => {
222                GuestEnv::decode(
223                    &fetch_url(
224                        std::str::from_utf8(&request.input.data)
225                            .context("input url is not utf8")?,
226                    )
227                    .await?,
228                )?
229                .stdin
230            }
231            _ => bail!("Unsupported input type"),
232        };
233        let order_receipt =
234            self.prove(order_elf.clone(), order_input, vec![], ProverOpts::succinct()).await?;
235        let order_journal = order_receipt.journal.bytes.clone();
236        let order_image_id = compute_image_id(&order_elf)?;
237
238        let fill = Fulfillment {
239            request: order.request.clone(),
240            signature: order.signature.into(),
241            journal: order_journal.clone(),
242            require_payment,
243        };
244
245        let assessor_receipt = self.assessor(vec![fill], vec![order_receipt.clone()]).await?;
246        let assessor_journal = assessor_receipt.journal.bytes.clone();
247        let assessor_image_id = compute_image_id(&self.assessor_elf)?;
248
249        let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
250        let order_claim_digest = order_claim.digest();
251        let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal);
252        let assessor_claim_digest = assessor_claim.digest();
253        let root_receipt = self
254            .finalize(
255                vec![order_claim.clone(), assessor_claim.clone()],
256                vec![order_receipt, assessor_receipt],
257            )
258            .await?;
259
260        let order_path = merkle_path(&[order_claim_digest, assessor_claim_digest], 0);
261        let assessor_path = merkle_path(&[order_claim_digest, assessor_claim_digest], 1);
262
263        let verifier_parameters =
264            SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
265
266        let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
267            order_claim,
268            order_path,
269            verifier_parameters.digest(),
270        );
271        let order_seal = order_inclusion_receipt.abi_encode_seal()?;
272
273        let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
274            assessor_claim,
275            assessor_path,
276            verifier_parameters.digest(),
277        );
278
279        let fulfillment = BoundlessFulfillment {
280            id: request.id,
281            requestDigest: order.request.eip712_signing_hash(&self.domain.alloy_struct()),
282            imageId: request.requirements.imageId,
283            journal: order_journal.into(),
284            requirePayment: require_payment,
285            seal: order_seal.into(),
286        };
287
288        Ok((fulfillment, root_receipt, order_inclusion_receipt, assessor_inclusion_receipt))
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use alloy::{primitives::PrimitiveSignature, signers::local::PrivateKeySigner};
296    use boundless_market::contracts::{
297        eip712_domain, Input, Offer, Predicate, ProofRequest, Requirements,
298    };
299    use guest_assessor::ASSESSOR_GUEST_ELF;
300    use guest_set_builder::SET_BUILDER_ELF;
301    use guest_util::{ECHO_ID, ECHO_PATH};
302    use risc0_zkvm::VerifierContext;
303
304    async fn setup_proving_request_and_signature(
305        signer: &PrivateKeySigner,
306    ) -> (ProofRequest, PrimitiveSignature) {
307        let request = ProofRequest::new(
308            0,
309            &signer.address(),
310            Requirements {
311                imageId: <[u8; 32]>::from(Digest::from(ECHO_ID)).into(),
312                predicate: Predicate::prefix_match(vec![1]),
313            },
314            format!("file://{ECHO_PATH}"),
315            Input::inline(vec![1, 2, 3, 4]),
316            Offer::default(),
317        );
318
319        let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
320        (request, signature)
321    }
322
323    #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
324    #[tokio::test]
325    async fn test_fulfill() {
326        let signer = PrivateKeySigner::random();
327        let (request, signature) = setup_proving_request_and_signature(&signer).await;
328
329        let domain = eip712_domain(Address::ZERO, 1);
330        let prover = DefaultProver::new(
331            SET_BUILDER_ELF.to_vec(),
332            ASSESSOR_GUEST_ELF.to_vec(),
333            Address::ZERO,
334            domain,
335        )
336        .expect("failed to create prover");
337
338        let order = Order { request, signature };
339        let (_, root_receipt, order_receipt, assessor_receipt) =
340            prover.fulfill(order.clone(), false).await.unwrap();
341
342        let verifier_parameters =
343            SetInclusionReceiptVerifierParameters { image_id: prover.set_builder_image_id };
344
345        order_receipt
346            .with_root(root_receipt.clone())
347            .verify_integrity_with_context(
348                &VerifierContext::default(),
349                verifier_parameters.clone(),
350                None,
351            )
352            .unwrap();
353        assessor_receipt
354            .with_root(root_receipt.clone())
355            .verify_integrity_with_context(&VerifierContext::default(), verifier_parameters, None)
356            .unwrap();
357    }
358}