risc0_zkvm/host/api/
client.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::path::Path;
16
17use anyhow::{anyhow, bail, Context, Result};
18use bytes::Bytes;
19use prost::Message;
20use risc0_zkp::core::digest::Digest;
21
22use super::{
23    malformed_err, pb, Asset, AssetRequest, ConnectionWrapper, Connector, ParentProcessConnector,
24    SessionInfo,
25};
26use crate::{
27    claim::receipt::UnionClaim,
28    get_version,
29    host::{api::SegmentInfo, client::prove::get_r0vm_path},
30    receipt::{AssumptionReceipt, SegmentReceipt, SuccinctReceipt},
31    ExecutorEnv, Journal, ProveInfo, ProverOpts, Receipt, ReceiptClaim, Unknown,
32};
33
34pub(crate) enum Compat {
35    Narrow,
36    Wide,
37}
38
39/// A client implementation for interacting with a zkVM server.
40pub struct Client {
41    connector: Box<dyn Connector>,
42    compat: Compat,
43    version_override: Option<semver::Version>,
44}
45
46impl Default for Client {
47    fn default() -> Self {
48        Self::new().unwrap()
49    }
50}
51
52impl Client {
53    /// Construct a [Client] that connects to `r0vm` in a child process.
54    pub fn new() -> Result<Self> {
55        Self::new_sub_process("r0vm")
56    }
57
58    /// Construct a [Client] that connects to a sub-process which implements
59    /// the server by calling the specified `server_path`.
60    pub fn new_sub_process<P: AsRef<Path>>(server_path: P) -> Result<Self> {
61        let connector = ParentProcessConnector::new_narrow_version(server_path)?;
62        Ok(Self::with_connector(Box::new(connector)))
63    }
64
65    /// Construct a [Client] that connects to a sub-process which implements
66    /// the server by calling the specified `server_path`.
67    ///
68    /// Additionally allows for wider version mismatches, only rejecting major differences.
69    pub fn new_sub_process_compat<P: AsRef<Path>>(
70        server_path: P,
71        version_override: semver::Version,
72    ) -> Result<Self> {
73        let connector = ParentProcessConnector::new(server_path)?;
74        Ok(Self {
75            connector: Box::new(connector),
76            compat: Compat::Wide,
77            version_override: Some(version_override),
78        })
79    }
80
81    /// Construct a [Client] based on environment variables.
82    pub fn from_env() -> Result<Self> {
83        Client::new_sub_process(get_r0vm_path()?)
84    }
85
86    /// Construct a [Client] using the specified [Connector] to establish a
87    /// connection with the server.
88    pub fn with_connector(connector: Box<dyn Connector>) -> Self {
89        Self {
90            connector,
91            compat: Compat::Narrow,
92            version_override: None,
93        }
94    }
95
96    /// Prove the specified ELF binary.
97    pub fn prove(
98        &self,
99        env: &ExecutorEnv<'_>,
100        opts: &ProverOpts,
101        binary: Asset,
102    ) -> Result<ProveInfo> {
103        let mut conn = self.connect()?;
104
105        let request = pb::api::ServerRequest {
106            kind: Some(pb::api::server_request::Kind::Prove(
107                pb::api::ProveRequest {
108                    env: Some(self.make_execute_env(env, binary.try_into()?)?),
109                    opts: Some(opts.clone().into()),
110                    receipt_out: Some(pb::api::AssetRequest {
111                        kind: Some(pb::api::asset_request::Kind::Inline(())),
112                    }),
113                },
114            )),
115        };
116        conn.send(request)?;
117
118        let asset = self.prove_handler(&mut conn, env)?;
119
120        let code = conn.close()?;
121        if code != 0 {
122            bail!("Child finished with: {code}");
123        }
124
125        let prove_info_bytes = asset.as_bytes()?;
126        let prove_info_pb = pb::core::ProveInfo::decode(prove_info_bytes)?;
127        prove_info_pb.try_into()
128    }
129
130    /// Execute the specified ELF binary.
131    pub fn execute<F>(
132        &self,
133        env: &ExecutorEnv<'_>,
134        binary: Asset,
135        segments_out: AssetRequest,
136        segment_callback: F,
137    ) -> Result<SessionInfo>
138    where
139        F: FnMut(SegmentInfo, Asset) -> Result<()>,
140    {
141        let mut conn = self.connect()?;
142
143        let request = pb::api::ServerRequest {
144            kind: Some(pb::api::server_request::Kind::Execute(
145                pb::api::ExecuteRequest {
146                    env: Some(self.make_execute_env(env, binary.try_into()?)?),
147                    segments_out: Some(segments_out.try_into()?),
148                },
149            )),
150        };
151        // tracing::trace!("tx: {request:?}");
152        conn.send(request)?;
153
154        let result = self.execute_handler(segment_callback, &mut conn, env);
155
156        let code = conn.close()?;
157        if code != 0 {
158            bail!("Child finished with: {code}");
159        }
160
161        result
162    }
163
164    /// Prove the specified segment.
165    pub fn prove_segment(
166        &self,
167        opts: &ProverOpts,
168        segment: Asset,
169        receipt_out: AssetRequest,
170    ) -> Result<SegmentReceipt> {
171        let mut conn = self.connect()?;
172
173        let request = pb::api::ServerRequest {
174            kind: Some(pb::api::server_request::Kind::ProveSegment(
175                pb::api::ProveSegmentRequest {
176                    opts: Some(opts.clone().into()),
177                    segment: Some(segment.try_into()?),
178                    receipt_out: Some(receipt_out.try_into()?),
179                },
180            )),
181        };
182        // tracing::trace!("tx: {request:?}");
183        conn.send(request).context("tx request failed")?;
184
185        let reply: pb::api::ProveSegmentReply = conn.recv().context("rx reply failed")?;
186
187        let result = match reply
188            .kind
189            .ok_or_else(|| malformed_err("ProveSegmentReply.kind"))?
190        {
191            pb::api::prove_segment_reply::Kind::Ok(result) => {
192                let receipt_bytes = result
193                    .receipt
194                    .ok_or_else(|| malformed_err("ProveSegmentReply.Ok.receipt"))?
195                    .as_bytes()?;
196                let receipt_pb = pb::core::SegmentReceipt::decode(receipt_bytes)?;
197                receipt_pb.try_into()
198            }
199            pb::api::prove_segment_reply::Kind::Error(err) => Err(err.into()),
200        };
201
202        let code = conn.close()?;
203        if code != 0 {
204            bail!("Child finished with: {code}");
205        }
206
207        result
208    }
209
210    /// Prove the specified keccak proof request.
211    pub fn prove_keccak(
212        &self,
213        proof_request: crate::host::client::env::ProveKeccakRequest,
214        receipt_out: AssetRequest,
215    ) -> Result<SuccinctReceipt<Unknown>> {
216        use crate::host::api::convert::keccak_input_to_bytes;
217
218        let mut conn = self.connect()?;
219
220        let request = pb::api::ServerRequest {
221            kind: Some(pb::api::server_request::Kind::ProveKeccak(
222                pb::api::ProveKeccakRequest {
223                    claim_digest: Some(proof_request.claim_digest.into()),
224                    po2: proof_request.po2 as u32,
225                    control_root: Some(proof_request.control_root.into()),
226                    input: keccak_input_to_bytes(&proof_request.input),
227                    receipt_out: Some(receipt_out.try_into()?),
228                },
229            )),
230        };
231
232        tracing::trace!("tx: {request:?}");
233        conn.send(request)?;
234
235        let reply: pb::api::ProveKeccakReply = conn.recv()?;
236
237        let result = match reply
238            .kind
239            .ok_or_else(|| malformed_err("ProveKeccakReply.kind"))?
240        {
241            pb::api::prove_keccak_reply::Kind::Ok(result) => {
242                let receipt_bytes = result
243                    .receipt
244                    .ok_or_else(|| malformed_err("ProveKeccakReply.Ok.receipt"))?
245                    .as_bytes()?;
246                let receipt_pb = pb::core::SuccinctReceipt::decode(receipt_bytes)?;
247                receipt_pb.try_into()
248            }
249            pb::api::prove_keccak_reply::Kind::Error(err) => Err(err.into()),
250        };
251
252        let code = conn.close()?;
253        if code != 0 {
254            bail!("Child finished with: {code}");
255        }
256
257        result
258    }
259
260    /// Run the lift program to transform a [SegmentReceipt] into a [SuccinctReceipt].
261    ///
262    /// The lift program verifies the rv32im circuit STARK proof inside the recursion circuit,
263    /// resulting in a recursion circuit STARK proof. This recursion proof has a single
264    /// constant-time verification procedure, with respect to the original segment length, and is then
265    /// used as the input to all other recursion programs (e.g. join, resolve, and identity_p254).
266    pub fn lift(
267        &self,
268        opts: &ProverOpts,
269        receipt: Asset,
270        receipt_out: AssetRequest,
271    ) -> Result<SuccinctReceipt<ReceiptClaim>> {
272        let mut conn = self.connect()?;
273
274        let request = pb::api::ServerRequest {
275            kind: Some(pb::api::server_request::Kind::Lift(pb::api::LiftRequest {
276                opts: Some(opts.clone().into()),
277                receipt: Some(receipt.try_into()?),
278                receipt_out: Some(receipt_out.try_into()?),
279            })),
280        };
281        // tracing::trace!("tx: {request:?}");
282        conn.send(request)?;
283
284        let reply: pb::api::LiftReply = conn.recv()?;
285
286        let result = match reply.kind.ok_or_else(|| malformed_err("LiftReply.kind"))? {
287            pb::api::lift_reply::Kind::Ok(result) => {
288                let receipt_bytes = result
289                    .receipt
290                    .ok_or_else(|| malformed_err("LiftReply.Ok.receipt"))?
291                    .as_bytes()?;
292                let receipt_pb = pb::core::SuccinctReceipt::decode(receipt_bytes)?;
293                receipt_pb.try_into()
294            }
295            pb::api::lift_reply::Kind::Error(err) => Err(err.into()),
296        };
297
298        let code = conn.close()?;
299        if code != 0 {
300            bail!("Child finished with: {code}");
301        }
302
303        result
304    }
305
306    /// Run the join program to compress two [SuccinctReceipt]s in the same session into one.
307    ///
308    /// By repeated application of the join program, any number of receipts for execution spans within
309    /// the same session can be compressed into a single receipt for the entire session.
310    pub fn join(
311        &self,
312        opts: &ProverOpts,
313        left_receipt: Asset,
314        right_receipt: Asset,
315        receipt_out: AssetRequest,
316    ) -> Result<SuccinctReceipt<ReceiptClaim>> {
317        let mut conn = self.connect()?;
318
319        let request = pb::api::ServerRequest {
320            kind: Some(pb::api::server_request::Kind::Join(pb::api::JoinRequest {
321                opts: Some(opts.clone().into()),
322                left_receipt: Some(left_receipt.try_into()?),
323                right_receipt: Some(right_receipt.try_into()?),
324                receipt_out: Some(receipt_out.try_into()?),
325            })),
326        };
327        // tracing::trace!("tx: {request:?}");
328        conn.send(request)?;
329
330        let reply: pb::api::JoinReply = conn.recv()?;
331
332        let result = match reply.kind.ok_or_else(|| malformed_err("JoinReply.kind"))? {
333            pb::api::join_reply::Kind::Ok(result) => {
334                let receipt_bytes = result
335                    .receipt
336                    .ok_or_else(|| malformed_err("JoinReply.Ok.receipt"))?
337                    .as_bytes()?;
338                let receipt_pb = pb::core::SuccinctReceipt::decode(receipt_bytes)?;
339                receipt_pb.try_into()
340            }
341            pb::api::join_reply::Kind::Error(err) => Err(err.into()),
342        };
343
344        let code = conn.close()?;
345        if code != 0 {
346            bail!("Child finished with: {code}");
347        }
348
349        result
350    }
351
352    /// Run the union program to compress any two [SuccinctReceipt]s into one.
353    ///
354    /// By repeated application of the union program, any number of receipts can
355    /// be compressed into a single receipt.
356    pub fn union(
357        &self,
358        opts: &ProverOpts,
359        a_receipt: Asset,
360        b_receipt: Asset,
361        receipt_out: AssetRequest,
362    ) -> Result<SuccinctReceipt<UnionClaim>> {
363        let mut conn = self.connect()?;
364
365        let request = pb::api::ServerRequest {
366            kind: Some(pb::api::server_request::Kind::Union(
367                pb::api::UnionRequest {
368                    opts: Some(opts.clone().into()),
369                    left_receipt: Some(a_receipt.try_into()?),
370                    right_receipt: Some(b_receipt.try_into()?),
371                    receipt_out: Some(receipt_out.try_into()?),
372                },
373            )),
374        };
375        // tracing::trace!("tx: {request:?}");
376        conn.send(request)?;
377
378        let reply: pb::api::UnionReply = conn.recv()?;
379
380        let result = match reply.kind.ok_or_else(|| malformed_err("UnionReply.kind"))? {
381            pb::api::union_reply::Kind::Ok(result) => {
382                let receipt_bytes = result
383                    .receipt
384                    .ok_or_else(|| malformed_err("UnionReply.Ok.receipt"))?
385                    .as_bytes()?;
386                let receipt_pb = pb::core::SuccinctReceipt::decode(receipt_bytes)?;
387                receipt_pb.try_into()
388            }
389            pb::api::union_reply::Kind::Error(err) => Err(err.into()),
390        };
391
392        let code = conn.close()?;
393        if code != 0 {
394            bail!("Child finished with: {code}");
395        }
396
397        result
398    }
399
400    /// Run the resolve program to remove an assumption from a conditional [SuccinctReceipt] upon
401    /// verifying a [SuccinctReceipt] proving the validity of the assumption.
402    ///
403    /// By applying the resolve program, a conditional receipt (i.e. a receipt for an execution
404    /// using the `env::verify` API to logically verify a receipt) can be made into an
405    /// unconditional receipt.
406    pub fn resolve(
407        &self,
408        opts: &ProverOpts,
409        conditional_receipt: Asset,
410        assumption_receipt: Asset,
411        receipt_out: AssetRequest,
412    ) -> Result<SuccinctReceipt<ReceiptClaim>> {
413        let mut conn = self.connect()?;
414
415        let request = pb::api::ServerRequest {
416            kind: Some(pb::api::server_request::Kind::Resolve(
417                pb::api::ResolveRequest {
418                    opts: Some(opts.clone().into()),
419                    conditional_receipt: Some(conditional_receipt.try_into()?),
420                    assumption_receipt: Some(assumption_receipt.try_into()?),
421                    receipt_out: Some(receipt_out.try_into()?),
422                },
423            )),
424        };
425        // tracing::trace!("tx: {request:?}");
426        conn.send(request)?;
427
428        let reply: pb::api::ResolveReply = conn.recv()?;
429
430        let result = match reply
431            .kind
432            .ok_or_else(|| malformed_err("ResolveReply.kind"))?
433        {
434            pb::api::resolve_reply::Kind::Ok(result) => {
435                let receipt_bytes = result
436                    .receipt
437                    .ok_or_else(|| malformed_err("ResolveReply.Ok.receipt"))?
438                    .as_bytes()?;
439                let receipt_pb = pb::core::SuccinctReceipt::decode(receipt_bytes)?;
440                receipt_pb.try_into()
441            }
442            pb::api::resolve_reply::Kind::Error(err) => Err(err.into()),
443        };
444
445        let code = conn.close()?;
446        if code != 0 {
447            bail!("Child finished with: {code}");
448        }
449
450        result
451    }
452
453    /// Prove the verification of a recursion receipt using the Poseidon254 hash function for FRI.
454    ///
455    /// The identity_p254 program is used as the last step in the prover pipeline before running the
456    /// Groth16 prover. In Groth16 over BN254, it is much more efficient to verify a STARK that was
457    /// produced with Poseidon over the BN254 base field compared to using Poseidon over BabyBear.
458    pub fn identity_p254(
459        &self,
460        opts: &ProverOpts,
461        receipt: Asset,
462        receipt_out: AssetRequest,
463    ) -> Result<SuccinctReceipt<ReceiptClaim>> {
464        let mut conn = self.connect()?;
465
466        let request = pb::api::ServerRequest {
467            kind: Some(pb::api::server_request::Kind::IdentityP254(
468                pb::api::IdentityP254Request {
469                    opts: Some(opts.clone().into()),
470                    receipt: Some(receipt.try_into()?),
471                    receipt_out: Some(receipt_out.try_into()?),
472                },
473            )),
474        };
475        // tracing::trace!("tx: {request:?}");
476        conn.send(request)?;
477
478        let reply: pb::api::IdentityP254Reply = conn.recv()?;
479
480        let result = match reply
481            .kind
482            .ok_or_else(|| malformed_err("IdentityP254Reply.kind"))?
483        {
484            pb::api::identity_p254_reply::Kind::Ok(result) => {
485                let receipt_bytes = result
486                    .receipt
487                    .ok_or_else(|| malformed_err("IdentityP254Reply.Ok.receipt"))?
488                    .as_bytes()?;
489                let receipt_pb = pb::core::SuccinctReceipt::decode(receipt_bytes)?;
490                receipt_pb.try_into()
491            }
492            pb::api::identity_p254_reply::Kind::Error(err) => Err(err.into()),
493        };
494
495        let code = conn.close()?;
496        if code != 0 {
497            bail!("Child finished with: {code}");
498        }
499
500        result
501    }
502
503    /// Compress a [Receipt], proving the same computation using a smaller representation.
504    ///
505    /// Proving will, by default, produce a [CompositeReceipt](crate::CompositeReceipt), which
506    /// may contain an arbitrary number of receipts assembled into segments and assumptions.
507    /// Together, these receipts collectively prove a top-level
508    /// [ReceiptClaim](crate::ReceiptClaim). This function can be used to compress all of the constituent
509    /// receipts of a [CompositeReceipt](crate::CompositeReceipt) into a single
510    /// [SuccinctReceipt](crate::SuccinctReceipt) or [Groth16Receipt](crate::Groth16Receipt) that proves the same top-level claim.
511    ///
512    /// Compression from [Groth16Receipt](crate::CompositeReceipt) to
513    /// [SuccinctReceipt](crate::SuccinctReceipt) is accomplished by iterative application of the
514    /// recursion programs including lift, join, and resolve.
515    ///
516    /// Compression from [SuccinctReceipt](crate::SuccinctReceipt) to
517    /// [Groth16Receipt](crate::Groth16Receipt) is accomplished by running a Groth16 recursive
518    /// verifier, referred to as the "STARK-to-SNARK" operation.
519    ///
520    /// NOTE: Compression to [Groth16Receipt](crate::Groth16Receipt) requires
521    /// Docker to be installed. See issue
522    /// [#1749](https://github.com/risc0/risc0/issues/1749) for more information.
523    ///
524    /// If the receipt is already at least as compressed as the requested compression level (e.g.
525    /// it is already succinct or Groth16 and a succinct receipt is required) this function is a
526    /// no-op. As a result, it is idempotent.
527    pub fn compress(
528        &self,
529        opts: &ProverOpts,
530        receipt: Asset,
531        receipt_out: AssetRequest,
532    ) -> Result<Receipt> {
533        let mut conn = self.connect()?;
534
535        let request = pb::api::ServerRequest {
536            kind: Some(pb::api::server_request::Kind::Compress(
537                pb::api::CompressRequest {
538                    opts: Some(opts.clone().into()),
539                    receipt: Some(receipt.try_into()?),
540                    receipt_out: Some(receipt_out.try_into()?),
541                },
542            )),
543        };
544        // tracing::trace!("tx: {request:?}");
545        conn.send(request)?;
546
547        let reply: pb::api::CompressReply = conn.recv()?;
548
549        let result = match reply
550            .kind
551            .ok_or_else(|| malformed_err("CompressReply.kind"))?
552        {
553            pb::api::compress_reply::Kind::Ok(result) => {
554                let receipt_bytes = result
555                    .receipt
556                    .ok_or_else(|| malformed_err("CompressReply.Ok.receipt"))?
557                    .as_bytes()?;
558                let receipt_pb = pb::core::Receipt::decode(receipt_bytes)?;
559                receipt_pb.try_into()
560            }
561            pb::api::compress_reply::Kind::Error(err) => Err(err.into()),
562        };
563
564        let code = conn.close()?;
565        if code != 0 {
566            bail!("Child finished with: {code}");
567        }
568
569        result
570    }
571
572    /// Verify a [Receipt].
573    pub fn verify(&self, receipt: Asset, image_id: impl Into<Digest>) -> Result<()> {
574        let mut conn = self.connect().context("connect")?;
575        let image_id = image_id.into();
576
577        let request = pb::api::ServerRequest {
578            kind: Some(pb::api::server_request::Kind::Verify(
579                pb::api::VerifyRequest {
580                    receipt: Some(receipt.try_into().context("convert receipt asset")?),
581                    image_id: Some(image_id.into()),
582                },
583            )),
584        };
585        // tracing::trace!("tx: {request:?}");
586        conn.send(request).context("send")?;
587
588        let reply: pb::api::GenericReply = conn.recv().context("error from server")?;
589        let result = match reply
590            .kind
591            .ok_or_else(|| malformed_err("GenericReply.kind"))?
592        {
593            pb::api::generic_reply::Kind::Ok(ok) => Ok(ok),
594            pb::api::generic_reply::Kind::Error(err) => Err(err.into()),
595        };
596
597        let code = conn.close().context("close")?;
598        if code != 0 {
599            bail!("Child finished with: {code}");
600        }
601
602        result
603    }
604
605    fn connect(&self) -> Result<ConnectionWrapper> {
606        let mut conn = self.connector.connect()?;
607
608        let client_version = get_version().map_err(|err| anyhow!(err))?;
609        let client_version = self.version_override.as_ref().unwrap_or(&client_version);
610        let request = pb::api::HelloRequest {
611            version: Some(client_version.clone().into()),
612        };
613        // tracing::trace!("tx: {request:?}");
614        conn.send(request)?;
615
616        let reply: pb::api::HelloReply = conn.recv()?;
617        // tracing::trace!("rx: {reply:?}");
618        match reply.kind.ok_or_else(|| malformed_err("HelloReply.kind"))? {
619            pb::api::hello_reply::Kind::Ok(reply) => {
620                let server_version: semver::Version = reply
621                    .version
622                    .ok_or_else(|| malformed_err("HelloReply.Ok.version"))?
623                    .try_into()
624                    .map_err(|err: semver::Error| anyhow!(err))?;
625
626                if !self.compat.check(client_version, &server_version) {
627                    let msg = format!("incompatible server version: {server_version}");
628                    tracing::warn!("{msg}");
629                    bail!(msg);
630                }
631            }
632            pb::api::hello_reply::Kind::Error(err) => {
633                let code = conn.close()?;
634                tracing::debug!("Child finished with: {code}");
635                bail!(err);
636            }
637        }
638
639        Ok(conn)
640    }
641
642    fn make_execute_env(
643        &self,
644        env: &ExecutorEnv<'_>,
645        binary: pb::api::Asset,
646    ) -> Result<pb::api::ExecutorEnv> {
647        Ok(pb::api::ExecutorEnv {
648            binary: Some(binary),
649            env_vars: env.env_vars.clone(),
650            args: env.args.clone(),
651            slice_ios: env.slice_io.borrow().inner.keys().cloned().collect(),
652            read_fds: env.posix_io.borrow().read_fds(),
653            write_fds: env.posix_io.borrow().write_fds(),
654            segment_limit_po2: env.segment_limit_po2,
655            keccak_max_po2: env.keccak_max_po2,
656            session_limit: env.session_limit,
657            trace_events: (!env.trace.is_empty()).then_some(()),
658            coprocessor: env.coprocessor.is_some(),
659            pprof_out: env
660                .pprof_out
661                .as_ref()
662                .map(|x| x.to_string_lossy().into())
663                .unwrap_or_default(),
664            assumptions: env
665                .assumptions
666                .borrow()
667                .0
668                .iter()
669                .map(|a| {
670                    Ok(match a {
671                        AssumptionReceipt::Proven(inner) => pb::api::AssumptionReceipt {
672                            kind: Some(pb::api::assumption_receipt::Kind::Proven(
673                                Asset::Inline(
674                                    pb::core::InnerReceipt::try_from(inner.clone())?
675                                        .encode_to_vec()
676                                        .into(),
677                                )
678                                .try_into()?,
679                            )),
680                        },
681                        AssumptionReceipt::Unresolved(assumption) => pb::api::AssumptionReceipt {
682                            kind: Some(pb::api::assumption_receipt::Kind::Unresolved(
683                                Asset::Inline(
684                                    pb::core::Assumption::try_from(assumption.clone())?
685                                        .encode_to_vec()
686                                        .into(),
687                                )
688                                .try_into()?,
689                            )),
690                        },
691                    })
692                })
693                .collect::<Result<_>>()?,
694            segment_path: env
695                .segment_path
696                .as_ref()
697                .map(|x| x.path().to_string_lossy().into())
698                .unwrap_or_default(),
699            povw_job_id: env
700                .povw_job_id
701                .map(|x| x.to_bytes().to_vec())
702                .unwrap_or_default(),
703        })
704    }
705
706    fn execute_handler<F>(
707        &self,
708        segment_callback: F,
709        conn: &mut ConnectionWrapper,
710        env: &ExecutorEnv<'_>,
711    ) -> Result<SessionInfo>
712    where
713        F: FnMut(SegmentInfo, Asset) -> Result<()>,
714    {
715        let mut segment_callback = segment_callback;
716        let mut segments = Vec::new();
717        loop {
718            let reply: pb::api::ServerReply = conn.recv()?;
719            // tracing::trace!("rx: {reply:?}");
720
721            match reply
722                .kind
723                .ok_or_else(|| malformed_err("ServerReply.kind"))?
724            {
725                pb::api::server_reply::Kind::Ok(request) => {
726                    match request
727                        .kind
728                        .ok_or_else(|| malformed_err("ServerReply.Ok.kind"))?
729                    {
730                        pb::api::client_callback::Kind::Io(io) => {
731                            let msg: pb::api::OnIoReply = self.on_io(env, io).into();
732                            // tracing::trace!("tx: {msg:?}");
733                            conn.send(msg)?;
734                        }
735                        pb::api::client_callback::Kind::SegmentDone(segment) => {
736                            let reply: pb::api::GenericReply = segment
737                                .segment
738                                .map_or_else(
739                                    || Err(malformed_err("ServerReply.Ok.SegmentDone.segment")),
740                                    |segment| {
741                                        let asset = segment
742                                            .segment
743                                            .ok_or_else(|| {
744                                                malformed_err(
745                                                    "ServerReply.Ok.SegmentDone.segment.segment",
746                                                )
747                                            })?
748                                            .try_into()?;
749                                        let info = SegmentInfo {
750                                            po2: segment.po2,
751                                            cycles: segment.cycles,
752                                        };
753                                        segments.push(info.clone());
754                                        segment_callback(info, asset)
755                                    },
756                                )
757                                .into();
758                            // tracing::trace!("tx: {reply:?}");
759                            conn.send(reply)?;
760                        }
761                        pb::api::client_callback::Kind::SessionDone(session) => {
762                            return match session.session {
763                                Some(session) => {
764                                    let receipt_claim = match session.receipt_claim {
765                                        Some(claim) => Some(
766                                            pb::core::ReceiptClaim::decode(claim.as_bytes()?)?
767                                                .try_into()?,
768                                        ),
769                                        None => None,
770                                    };
771
772                                    Ok(SessionInfo {
773                                        segments,
774                                        journal: Journal::new(session.journal),
775                                        exit_code: session
776                                            .exit_code
777                                            .ok_or_else(|| malformed_err("SessionInfo.exit_code"))?
778                                            .try_into()?,
779                                        receipt_claim,
780                                    })
781                                }
782                                None => Err(malformed_err("ServerReply.ok.SessionDone.session")),
783                            }
784                        }
785                        pb::api::client_callback::Kind::ProveDone(_) => {
786                            return Err(anyhow!("Illegal client callback"))
787                        }
788                    }
789                }
790                pb::api::server_reply::Kind::Error(err) => return Err(err.into()),
791            }
792        }
793    }
794
795    fn prove_handler(
796        &self,
797        conn: &mut ConnectionWrapper,
798        env: &ExecutorEnv<'_>,
799    ) -> Result<pb::api::Asset> {
800        loop {
801            let reply: pb::api::ServerReply = conn.recv()?;
802            // tracing::trace!("rx: {reply:?}");
803
804            match reply
805                .kind
806                .ok_or_else(|| malformed_err("ServerReply.kind"))?
807            {
808                pb::api::server_reply::Kind::Ok(request) => {
809                    match request
810                        .kind
811                        .ok_or_else(|| malformed_err("ServerReply.Ok.kind"))?
812                    {
813                        pb::api::client_callback::Kind::Io(io) => {
814                            let msg: pb::api::OnIoReply = self.on_io(env, io).into();
815                            // tracing::trace!("tx: {msg:?}");
816                            conn.send(msg)?;
817                        }
818                        pb::api::client_callback::Kind::SegmentDone(_) => {
819                            return Err(anyhow!("Illegal client callback"))
820                        }
821                        pb::api::client_callback::Kind::SessionDone(_) => {
822                            return Err(anyhow!("Illegal client callback"))
823                        }
824                        pb::api::client_callback::Kind::ProveDone(done) => {
825                            return done.prove_info.ok_or_else(|| {
826                                malformed_err("ServerReply.Ok.ProveDone.prove_info")
827                            })
828                        }
829                    }
830                }
831                pb::api::server_reply::Kind::Error(err) => return Err(err.into()),
832            }
833        }
834    }
835
836    fn on_io(&self, env: &ExecutorEnv<'_>, request: pb::api::OnIoRequest) -> Result<Bytes> {
837        match request
838            .kind
839            .ok_or_else(|| malformed_err("OnIoRequest.kind"))?
840        {
841            pb::api::on_io_request::Kind::Posix(posix) => {
842                let cmd = posix
843                    .cmd
844                    .ok_or_else(|| malformed_err("OnIoRequest.Posix.cmd"))?;
845                match cmd
846                    .kind
847                    .ok_or_else(|| malformed_err("OnIoRequest.Posix.cmd.kind"))?
848                {
849                    pb::api::posix_cmd::Kind::Read(nread) => {
850                        self.on_posix_read(env, posix.fd, nread as usize)
851                    }
852                    pb::api::posix_cmd::Kind::Write(from_guest) => {
853                        self.on_posix_write(env, posix.fd, from_guest.into())?;
854                        Ok(Bytes::new())
855                    }
856                }
857            }
858            pb::api::on_io_request::Kind::Slice(slice_io) => {
859                self.on_slice(env, &slice_io.name, slice_io.from_guest.into())
860            }
861            pb::api::on_io_request::Kind::Trace(event) => {
862                self.on_trace(env, event)?;
863                Ok(Bytes::new())
864            }
865            pb::api::on_io_request::Kind::Coprocessor(request) => {
866                self.on_coprocessor(env, request)?;
867                Ok(Bytes::new())
868            }
869        }
870    }
871
872    fn on_posix_read(&self, env: &ExecutorEnv<'_>, fd: u32, nread: usize) -> Result<Bytes> {
873        tracing::debug!("on_posix_read: {fd}, {nread}");
874        let mut from_host = vec![0; nread];
875        let posix_io = env.posix_io.borrow();
876        let reader = posix_io.get_reader(fd)?;
877        let nread = reader.borrow_mut().read(&mut from_host)?;
878        let slice = from_host[..nread].to_vec();
879        Ok(slice.into())
880    }
881
882    fn on_posix_write(&self, env: &ExecutorEnv<'_>, fd: u32, from_guest: Bytes) -> Result<()> {
883        tracing::debug!("on_posix_write: {fd}");
884        let posix_io = env.posix_io.borrow();
885        let writer = posix_io.get_writer(fd)?;
886        writer.borrow_mut().write_all(&from_guest)?;
887        Ok(())
888    }
889
890    fn on_slice(&self, env: &ExecutorEnv<'_>, name: &str, from_guest: Bytes) -> Result<Bytes> {
891        let table = env.slice_io.borrow();
892        let slice_io = table
893            .inner
894            .get(name)
895            .ok_or_else(|| anyhow!("Unknown I/O channel name: {name}"))?;
896        let result = slice_io.borrow_mut().handle_io(name, from_guest)?;
897        Ok(result)
898    }
899
900    fn on_trace(&self, env: &ExecutorEnv<'_>, event: pb::api::TraceEvent) -> Result<()> {
901        for trace_callback in env.trace.iter() {
902            trace_callback
903                .borrow_mut()
904                .trace_callback(event.clone().try_into()?)?;
905        }
906        Ok(())
907    }
908
909    fn on_coprocessor(
910        &self,
911        env: &ExecutorEnv<'_>,
912        coprocessor_request: pb::api::CoprocessorRequest,
913    ) -> Result<()> {
914        match coprocessor_request
915            .kind
916            .ok_or_else(|| malformed_err("OnCoprocessorRequest.kind"))?
917        {
918            pb::api::coprocessor_request::Kind::ProveKeccak(proof_request) => {
919                let proof_request = proof_request.try_into()?;
920                let coprocessor = env
921                    .coprocessor
922                    .clone()
923                    .ok_or_else(|| malformed_err("OnCoprocessorRequest.ProveKeccak.coprocessor"))?;
924                let mut coprocessor = coprocessor.borrow_mut();
925                coprocessor.prove_keccak(proof_request)
926            }
927        }
928    }
929}
930
931impl From<Result<Bytes, anyhow::Error>> for pb::api::OnIoReply {
932    fn from(result: Result<Bytes, anyhow::Error>) -> Self {
933        Self {
934            kind: Some(match result {
935                Ok(bytes) => pb::api::on_io_reply::Kind::Ok(bytes.into()),
936                Err(err) => pb::api::on_io_reply::Kind::Error(err.into()),
937            }),
938        }
939    }
940}
941
942impl Compat {
943    pub(crate) fn check(&self, requested: &semver::Version, server: &semver::Version) -> bool {
944        match self {
945            Compat::Narrow => {
946                if requested.pre.is_empty() {
947                    requested.major == server.major && requested.minor == server.minor
948                } else {
949                    requested == server
950                }
951            }
952            Compat::Wide => requested.major == server.major,
953        }
954    }
955}
956
957#[cfg(test)]
958mod tests {
959    use semver::Version;
960
961    use super::Compat;
962
963    #[test]
964    fn check_version_narrow() {
965        fn test(requested: &str, server: &str) -> bool {
966            Compat::Narrow.check(
967                &Version::parse(requested).unwrap(),
968                &Version::parse(server).unwrap(),
969            )
970        }
971
972        assert!(test("0.18.0", "0.18.0"));
973        assert!(test("0.18.0", "0.18.1"));
974        assert!(test("0.18.1", "0.18.1"));
975        assert!(test("0.18.1", "0.18.2"));
976        assert!(test("0.18.1", "0.18.0"));
977        assert!(!test("0.18.0", "0.19.0"));
978
979        assert!(test("1.0.0", "1.0.0"));
980        assert!(test("1.0.0", "1.0.1"));
981        assert!(test("1.1.0", "1.1.0"));
982        assert!(test("1.1.0", "1.1.1"));
983        assert!(!test("1.0.0", "0.18.0"));
984        assert!(!test("1.0.0", "2.0.0"));
985        assert!(!test("1.1.0", "1.0.0"));
986        assert!(test("1.0.3", "1.0.1"));
987
988        assert!(test("0.19.0-alpha.1", "0.19.0-alpha.1"));
989        assert!(!test("0.19.0-alpha.1", "0.19.0-alpha.2"));
990    }
991
992    #[test]
993    fn check_wide_version() {
994        fn test(requested: &str, server: &str) -> bool {
995            Compat::Wide.check(
996                &Version::parse(requested).unwrap(),
997                &Version::parse(server).unwrap(),
998            )
999        }
1000
1001        assert!(test("0.1.0", "0.1.0"));
1002        assert!(test("0.1.0", "0.1.1"));
1003        assert!(test("0.1.0", "0.2.0"));
1004        assert!(test("0.1.0-rc.1", "0.2.0"));
1005        assert!(test("1.1.0", "1.0.0"));
1006        assert!(!test("1.0.0", "2.0.0"));
1007    }
1008}