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