risc0_zkvm/host/client/prove/
bonsai.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
15use std::time::Duration;
16
17use anyhow::{anyhow, bail, ensure, Context, Result};
18use bonsai_sdk::blocking::Client;
19
20use super::Prover;
21use crate::{
22    compute_image_id, AssumptionReceipt, ExecutorEnv, InnerAssumptionReceipt, InnerReceipt,
23    ProveInfo, ProverOpts, Receipt, ReceiptKind, SessionStats, VerifierContext, VERSION,
24};
25
26/// An implementation of a [Prover] that runs proof workloads via Bonsai.
27///
28/// Requires `BONSAI_API_URL` and `BONSAI_API_KEY` environment variables to
29/// submit proving sessions to Bonsai.
30pub struct BonsaiProver {
31    name: String,
32}
33
34impl BonsaiProver {
35    /// Construct a [BonsaiProver].
36    pub fn new(name: &str) -> Self {
37        Self {
38            name: name.to_string(),
39        }
40    }
41}
42
43// Only proven assumptions that are succinct are supported by Bonsai.
44fn get_inner_assumption_receipt(assumption: &AssumptionReceipt) -> Result<&InnerAssumptionReceipt> {
45    match assumption {
46        AssumptionReceipt::Proven(receipt) => {
47            if !matches!(receipt, InnerAssumptionReceipt::Succinct(_)) {
48                bail!(
49                    "Bonsai only supports succinct assumption receipts. \
50                    Use `ProverOpts::succinct()` when proving any assumptions."
51                );
52            };
53            Ok(receipt)
54        }
55        AssumptionReceipt::Unresolved(_) => {
56            bail!("only proven assumptions can be uploaded to Bonsai.")
57        }
58    }
59}
60
61impl Prover for BonsaiProver {
62    fn get_name(&self) -> String {
63        self.name.clone()
64    }
65
66    fn prove_with_ctx(
67        &self,
68        env: ExecutorEnv<'_>,
69        ctx: &VerifierContext,
70        elf: &[u8],
71        opts: &ProverOpts,
72    ) -> Result<ProveInfo> {
73        let client = Client::from_env(VERSION)?;
74
75        // Compute the ImageID and upload the ELF binary
76        let image_id = compute_image_id(elf)?;
77        let image_id_hex = hex::encode(image_id);
78        client.upload_img(&image_id_hex, elf.to_vec())?;
79
80        // upload input data
81        let input_id = client.upload_input(env.input)?;
82
83        // upload receipts
84        let mut receipts_ids = vec![];
85        for assumption in env.assumptions.borrow().0.iter() {
86            let inner_receipt = get_inner_assumption_receipt(assumption)?;
87            let serialized_receipt = bincode::serialize(inner_receipt)?;
88            let receipt_id = client.upload_receipt(serialized_receipt)?;
89            receipts_ids.push(receipt_id);
90        }
91
92        // While this is the executor, we want to start a session on the bonsai prover.
93        // By doing so, we can return a session ID so that the prover can use it to
94        // retrieve the receipt.
95        let session = client.create_session_with_limit(
96            image_id_hex,
97            input_id,
98            receipts_ids,
99            false,
100            env.session_limit,
101        )?;
102        tracing::debug!("Bonsai proving SessionID: {}", session.uuid);
103
104        // TODO(#1759): Improve upon this polling solution.
105        let polling_interval = if let Ok(ms) = std::env::var("BONSAI_POLL_INTERVAL_MS") {
106            Duration::from_millis(ms.parse().context("invalid bonsai poll interval")?)
107        } else {
108            Duration::from_secs(1)
109        };
110        let succinct_prove_info = loop {
111            // The session has already been started in the executor. Poll bonsai to check if
112            // the proof request succeeded.
113            let res = session.status(&client)?;
114            if res.status == "RUNNING" {
115                std::thread::sleep(polling_interval);
116                continue;
117            }
118            if res.status == "SUCCEEDED" {
119                // Download the receipt, containing the output
120                let receipt_url = res
121                    .receipt_url
122                    .ok_or_else(|| anyhow!("API error, missing receipt on completed session"))?;
123
124                let stats = res
125                    .stats
126                    .context("Missing stats object on Bonsai status res")?;
127                tracing::debug!(
128                    "Bonsai usage: cycles: {} total_cycles: {}",
129                    stats.cycles,
130                    stats.total_cycles
131                );
132
133                let receipt_buf = client.download(&receipt_url)?;
134                let receipt: Receipt = bincode::deserialize(&receipt_buf)?;
135
136                if opts.prove_guest_errors {
137                    receipt.verify_integrity_with_context(ctx)?;
138                } else {
139                    receipt.verify_with_context(ctx, image_id)?;
140                }
141                break ProveInfo {
142                    receipt,
143                    work_receipt: None,
144                    stats: SessionStats {
145                        segments: stats.segments,
146                        total_cycles: stats.total_cycles,
147                        user_cycles: stats.cycles,
148                        // These are currently unavailable from Bonsai
149                        paging_cycles: 0,
150                        reserved_cycles: 0,
151                    },
152                };
153            } else {
154                bail!(
155                    "Bonsai prover workflow [{}] exited: {} err: {}",
156                    session.uuid,
157                    res.status,
158                    res.error_msg
159                        .unwrap_or("Bonsai workflow missing error_msg".into()),
160                );
161            }
162        };
163        match opts.receipt_kind {
164            // If the caller requested a composite or succinct receipt, we are done.
165            ReceiptKind::Composite | ReceiptKind::Succinct => {
166                return Ok(succinct_prove_info);
167            }
168            // If they requested a groth16 receipts, we need to continue.
169            ReceiptKind::Groth16 => {}
170        }
171
172        // Request that Bonsai compress further, to Groth16.
173        let snark_session = client.create_snark(session.uuid)?;
174        let snark_receipt_url = loop {
175            let res = snark_session.status(&client)?;
176            match res.status.as_str() {
177                "RUNNING" => {
178                    std::thread::sleep(polling_interval);
179                    continue;
180                }
181                "SUCCEEDED" => {
182                    break res.output.with_context(|| {
183                        format!(
184                            "Bonsai prover workflow [{}] reported success, but provided no receipt",
185                            snark_session.uuid
186                        )
187                    })?;
188                }
189                _ => {
190                    bail!(
191                        "Bonsai prover workflow [{}] exited: {} err: {}",
192                        snark_session.uuid,
193                        res.status,
194                        res.error_msg
195                            .unwrap_or("Bonsai workflow missing error_msg".into()),
196                    );
197                }
198            }
199        };
200
201        // Assemble the groth16 receipt, and verify it.
202        // TODO(bonsai-alpha#461): If the Groth16 parameters used by Bonsai do not match, this
203        // verification will fail. When Bonsai returned ReceiptMetadata, we'll be able to detect
204        // this error condition and report a better message. Constructing the receipt here will all
205        // the verifier parameters for this version of risc0-zkvm, which may be different than
206        // Bonsai. By verifying the receipt though, we at least know the proving key used on Bonsai
207        // matches the verifying key used here.
208        let receipt_buf = client.download(&snark_receipt_url)?;
209        let groth16_receipt: Receipt = bincode::deserialize(&receipt_buf)?;
210        groth16_receipt
211            .verify_integrity_with_context(ctx)
212            .context("failed to verify Groth16Receipt returned by Bonsai")?;
213
214        // Return the groth16 receipt, with the stats collected earlier.
215        Ok(ProveInfo {
216            receipt: groth16_receipt,
217            work_receipt: None,
218            stats: succinct_prove_info.stats,
219        })
220    }
221
222    fn compress(&self, opts: &ProverOpts, receipt: &Receipt) -> Result<Receipt> {
223        match (&receipt.inner, opts.receipt_kind) {
224            // Compression is a no-op when the requested kind is at least as large as the current.
225            (InnerReceipt::Composite(_), ReceiptKind::Composite)
226            | (InnerReceipt::Succinct(_), ReceiptKind::Composite | ReceiptKind::Succinct)
227            | (
228                InnerReceipt::Groth16(_),
229                ReceiptKind::Composite | ReceiptKind::Succinct | ReceiptKind::Groth16,
230            ) => Ok(receipt.clone()),
231            // Compression is always a no-op in dev mode
232            (InnerReceipt::Fake { .. }, _) => {
233                ensure!(
234                    opts.dev_mode(),
235                    "dev mode must be enabled to compress fake receipts"
236                );
237                Ok(receipt.clone())
238            }
239            // NOTE: Bonsai always returns a SuccinctReceipt, and does not currently support
240            // compression of existing receipts uploaded by clients.
241            (_, ReceiptKind::Succinct) => {
242                bail!("BonsaiProver does not support compression on existing receipts");
243            }
244            (_, ReceiptKind::Groth16) => {
245                // Caller is requesting a Groth16Receipt. Provide a hint on how to get one.
246                bail!([
247                    "BonsaiProver does not support compression on existing receipts",
248                    "Set receipt_kind on ProverOpts in initial prove request to get a Groth16Receipt."
249                ].join(""));
250            }
251        }
252    }
253}