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};
36
37use boundless_market::{
38    contracts::{
39        AssessorJournal, AssessorReceipt, EIP712DomainSaltless,
40        Fulfillment as BoundlessFulfillment, RequestInputType,
41    },
42    input::GuestEnv,
43    order_stream_client::Order,
44    selector::{is_groth16_selector, SupportedSelectors},
45    storage::fetch_url,
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/// The default prover implementation.
91/// This [DefaultProver] uses the default zkVM prover.
92/// The selection of the zkVM prover is based on environment variables.
93///
94/// The `RISC0_PROVER` environment variable, if specified, will select the
95/// following [Prover] implementation:
96/// * `bonsai`: [BonsaiProver] to prove on Bonsai.
97/// * `local`: LocalProver to prove locally in-process. Note: this
98///   requires the `prove` feature flag.
99/// * `ipc`: [ExternalProver] to prove using an `r0vm` sub-process. Note: `r0vm`
100///   must be installed. To specify the path to `r0vm`, use `RISC0_SERVER_PATH`.
101///
102/// If `RISC0_PROVER` is not specified, the following rules are used to select a
103/// [Prover]:
104/// * [BonsaiProver] if the `BONSAI_API_URL` and `BONSAI_API_KEY` environment
105///   variables are set unless `RISC0_DEV_MODE` is enabled.
106/// * LocalProver if the `prove` feature flag is enabled.
107/// * [ExternalProver] otherwise.
108pub struct DefaultProver {
109    set_builder_program: Vec<u8>,
110    set_builder_image_id: Digest,
111    assessor_program: Vec<u8>,
112    address: Address,
113    domain: EIP712DomainSaltless,
114    supported_selectors: SupportedSelectors,
115}
116
117impl DefaultProver {
118    /// Creates a new [DefaultProver].
119    pub fn new(
120        set_builder_program: Vec<u8>,
121        assessor_program: Vec<u8>,
122        address: Address,
123        domain: EIP712DomainSaltless,
124    ) -> Result<Self> {
125        let set_builder_image_id = compute_image_id(&set_builder_program)?;
126        let supported_selectors =
127            SupportedSelectors::default().with_set_builder_image_id(set_builder_image_id);
128        Ok(Self {
129            set_builder_program,
130            set_builder_image_id,
131            assessor_program,
132            address,
133            domain,
134            supported_selectors,
135        })
136    }
137
138    // Proves the given [program] with the given [input] and [assumptions].
139    // The [opts] parameter specifies the prover options.
140    pub(crate) async fn prove(
141        &self,
142        program: Vec<u8>,
143        input: Vec<u8>,
144        assumptions: Vec<Receipt>,
145        opts: ProverOpts,
146    ) -> Result<Receipt> {
147        let receipt = tokio::task::spawn_blocking(move || {
148            let mut env = ExecutorEnv::builder();
149            env.write_slice(&input);
150            for assumption_receipt in assumptions.iter() {
151                env.add_assumption(assumption_receipt.clone());
152            }
153            let env = env.build()?;
154
155            default_prover().prove_with_opts(env, &program, &opts)
156        })
157        .await??
158        .receipt;
159        Ok(receipt)
160    }
161
162    pub(crate) async fn compress(&self, succinct_receipt: &Receipt) -> Result<Receipt> {
163        let prover = default_prover();
164        if prover.get_name() == "bonsai" {
165            return compress_with_bonsai(succinct_receipt).await;
166        }
167        if is_dev_mode() {
168            return Ok(succinct_receipt.clone());
169        }
170
171        let receipt = succinct_receipt.clone();
172        tokio::task::spawn_blocking(move || {
173            default_prover().compress(&ProverOpts::groth16(), &receipt)
174        })
175        .await?
176    }
177
178    // Finalizes the set builder.
179    pub(crate) async fn finalize(
180        &self,
181        claims: Vec<ReceiptClaim>,
182        assumptions: Vec<Receipt>,
183    ) -> Result<Receipt> {
184        let input = GuestState::initial(self.set_builder_image_id)
185            .into_input(claims, true)
186            .context("Failed to build set builder input")?;
187        let encoded_input = bytemuck::pod_collect_to_vec(&risc0_zkvm::serde::to_vec(&input)?);
188
189        self.prove(
190            self.set_builder_program.clone(),
191            encoded_input,
192            assumptions,
193            ProverOpts::groth16(),
194        )
195        .await
196    }
197
198    // Proves the assessor.
199    pub(crate) async fn assessor(
200        &self,
201        fills: Vec<Fulfillment>,
202        receipts: Vec<Receipt>,
203    ) -> Result<Receipt> {
204        let assessor_input =
205            AssessorInput { domain: self.domain.clone(), fills, prover_address: self.address };
206
207        let stdin = GuestEnv::builder().write_frame(&assessor_input.encode()).stdin;
208
209        self.prove(self.assessor_program.clone(), stdin, receipts, ProverOpts::succinct()).await
210    }
211
212    /// Fulfills a list of orders, returning the relevant data:
213    /// * A list of [Fulfillment] of the orders.
214    /// * The [Receipt] of the root set.
215    /// * The [SetInclusionReceipt] of the assessor.
216    pub async fn fulfill(
217        &self,
218        orders: &[Order],
219    ) -> Result<(Vec<BoundlessFulfillment>, Receipt, AssessorReceipt)> {
220        let orders_jobs = orders.iter().map(|order| async {
221            let request = order.request.clone();
222            let order_program = fetch_url(&request.imageUrl).await?;
223            let order_input: Vec<u8> = match request.input.inputType {
224                RequestInputType::Inline => GuestEnv::decode(&request.input.data)?.stdin,
225                RequestInputType::Url => {
226                    GuestEnv::decode(
227                        &fetch_url(
228                            std::str::from_utf8(&request.input.data)
229                                .context("input url is not utf8")?,
230                        )
231                        .await?,
232                    )?
233                    .stdin
234                }
235                _ => bail!("Unsupported input type"),
236            };
237
238            let selector = request.requirements.selector;
239            if !self.supported_selectors.is_supported(selector) {
240                bail!("Unsupported selector {}", request.requirements.selector);
241            };
242
243            let order_receipt = self
244                .prove(order_program.clone(), order_input.clone(), vec![], ProverOpts::succinct())
245                .await?;
246
247            let order_journal = order_receipt.journal.bytes.clone();
248            let order_image_id = compute_image_id(&order_program)?;
249            let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
250            let order_claim_digest = order_claim.digest();
251
252            let fill = Fulfillment {
253                request: order.request.clone(),
254                signature: order.signature.into(),
255                journal: order_journal.clone(),
256            };
257
258            Ok::<_, anyhow::Error>((order_receipt, order_claim, order_claim_digest, fill))
259        });
260
261        let results = futures::future::join_all(orders_jobs).await;
262        let mut receipts = Vec::new();
263        let mut claims = Vec::new();
264        let mut claim_digests = Vec::new();
265        let mut fills = Vec::new();
266
267        for (i, result) in results.into_iter().enumerate() {
268            if let Err(e) = result {
269                tracing::warn!("Failed to prove request 0x{:x}: {}", orders[i].request.id, e);
270                continue;
271            }
272            let (receipt, claim, claim_digest, fill) = result?;
273            receipts.push(receipt);
274            claims.push(claim);
275            claim_digests.push(claim_digest);
276            fills.push(fill);
277        }
278
279        let assessor_receipt = self.assessor(fills.clone(), receipts.clone()).await?;
280        let assessor_journal = assessor_receipt.journal.bytes.clone();
281        let assessor_image_id = compute_image_id(&self.assessor_program)?;
282        let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal.clone());
283        let assessor_receipt_journal: AssessorJournal =
284            AssessorJournal::abi_decode(&assessor_journal)?;
285
286        receipts.push(assessor_receipt);
287        claims.push(assessor_claim.clone());
288        claim_digests.push(assessor_claim.digest());
289
290        let root_receipt = self.finalize(claims.clone(), receipts.clone()).await?;
291
292        let verifier_parameters =
293            SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
294
295        let mut boundless_fills = Vec::new();
296
297        for i in 0..fills.len() {
298            let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
299                claims[i].clone(),
300                merkle_path(&claim_digests, i),
301                verifier_parameters.digest(),
302            );
303            let order = &orders[i];
304            let order_seal = if is_groth16_selector(order.request.requirements.selector) {
305                let receipt = self.compress(&receipts[i]).await?;
306                encode_seal(&receipt)?
307            } else {
308                order_inclusion_receipt.abi_encode_seal()?
309            };
310
311            let fulfillment = BoundlessFulfillment {
312                id: order.request.id,
313                requestDigest: order.request.eip712_signing_hash(&self.domain.alloy_struct()),
314                imageId: order.request.requirements.imageId,
315                journal: fills[i].journal.clone().into(),
316                seal: order_seal.into(),
317            };
318
319            boundless_fills.push(fulfillment);
320        }
321
322        let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
323            assessor_claim,
324            merkle_path(&claim_digests, claim_digests.len() - 1),
325            verifier_parameters.digest(),
326        );
327
328        let assessor_receipt = AssessorReceipt {
329            seal: assessor_inclusion_receipt.abi_encode_seal()?.into(),
330            prover: self.address,
331            selectors: assessor_receipt_journal.selectors,
332            callbacks: assessor_receipt_journal.callbacks,
333        };
334
335        Ok((boundless_fills, root_receipt, assessor_receipt))
336    }
337}
338
339async fn compress_with_bonsai(succinct_receipt: &Receipt) -> Result<Receipt> {
340    let client = BonsaiClient::from_env(risc0_zkvm::VERSION)?;
341    let encoded_receipt = bincode::serialize(succinct_receipt)?;
342    let receipt_id = client.upload_receipt(encoded_receipt).await?;
343    let snark_id = client.create_snark(receipt_id).await?;
344    loop {
345        let status = snark_id.status(&client).await?;
346        match status.status.as_ref() {
347            "RUNNING" => {
348                tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
349                continue;
350            }
351            "SUCCEEDED" => {
352                let receipt_buf = client.download(&status.output.unwrap()).await?;
353                let snark_receipt: Receipt = bincode::deserialize(&receipt_buf)?;
354                return Ok(snark_receipt);
355            }
356            _ => {
357                let err_msg = status.error_msg.unwrap_or_default();
358                return Err(anyhow::anyhow!("snark proving failed: {err_msg}"));
359            }
360        }
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use alloy::{
368        primitives::{FixedBytes, Signature},
369        signers::local::PrivateKeySigner,
370    };
371    use boundless_market::contracts::{
372        eip712_domain, Offer, Predicate, ProofRequest, RequestId, RequestInput, Requirements,
373        UNSPECIFIED_SELECTOR,
374    };
375    use boundless_market_test_utils::{ASSESSOR_GUEST_ELF, ECHO_ID, ECHO_PATH, SET_BUILDER_ELF};
376    use risc0_ethereum_contracts::selector::Selector;
377
378    async fn setup_proving_request_and_signature(
379        signer: &PrivateKeySigner,
380        selector: Option<Selector>,
381    ) -> (ProofRequest, Signature) {
382        let request = ProofRequest::new(
383            RequestId::new(signer.address(), 0),
384            Requirements::new(Digest::from(ECHO_ID), Predicate::prefix_match(vec![1]))
385                .with_selector(match selector {
386                    Some(selector) => FixedBytes::from(selector as u32),
387                    None => UNSPECIFIED_SELECTOR,
388                }),
389            format!("file://{ECHO_PATH}"),
390            RequestInput::builder().write_slice(&[1, 2, 3, 4]).build_inline().unwrap(),
391            Offer::default(),
392        );
393
394        let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
395        (request, signature)
396    }
397
398    #[tokio::test]
399    #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
400    async fn test_fulfill_with_selector() {
401        let signer = PrivateKeySigner::random();
402        let (request, signature) =
403            setup_proving_request_and_signature(&signer, Some(Selector::Groth16V2_0)).await;
404
405        let domain = eip712_domain(Address::ZERO, 1);
406        let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
407        let prover = DefaultProver::new(
408            SET_BUILDER_ELF.to_vec(),
409            ASSESSOR_GUEST_ELF.to_vec(),
410            Address::ZERO,
411            domain,
412        )
413        .expect("failed to create prover");
414
415        let order = Order { request, request_digest, signature };
416        prover.fulfill(&[order.clone()]).await.unwrap();
417    }
418
419    #[tokio::test]
420    #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
421    async fn test_fulfill() {
422        let signer = PrivateKeySigner::random();
423        let (request, signature) = setup_proving_request_and_signature(&signer, None).await;
424
425        let domain = eip712_domain(Address::ZERO, 1);
426        let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
427        let prover = DefaultProver::new(
428            SET_BUILDER_ELF.to_vec(),
429            ASSESSOR_GUEST_ELF.to_vec(),
430            Address::ZERO,
431            domain,
432        )
433        .expect("failed to create prover");
434
435        let order = Order { request, request_digest, signature };
436        prover.fulfill(&[order.clone()]).await.unwrap();
437    }
438}