Skip to main content

boundless_market/request_builder/
preflight_layer.rs

1// Copyright 2026 Boundless Foundation, 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 super::{Adapt, RequestParams};
16use crate::{
17    contracts::{RequestInput, RequestInputType},
18    input::GuestEnv,
19    prover_utils::local_executor::LocalExecutor,
20    storage::StorageDownloader,
21    NotProvided,
22};
23use anyhow::{bail, ensure, Context};
24
25/// A layer that performs preflight execution of the guest program.
26///
27/// This layer runs the program with the provided input to compute:
28/// - The journal output
29/// - The cycle count
30/// - The image ID
31///
32/// Running the program in advance allows for proper pricing estimation and
33/// verification configuration based on actual execution results.
34///
35/// Uses a LocalExecutor for execution, which deduplicates executions by
36/// content-addressing (same program + input = same result returned from cache).
37#[non_exhaustive]
38#[derive(Clone)]
39pub struct PreflightLayer<D> {
40    executor: LocalExecutor,
41    /// The downloader used to fetch programs and inputs from URLs.
42    downloader: Option<D>,
43}
44
45impl<D> Default for PreflightLayer<D> {
46    fn default() -> Self {
47        Self { executor: LocalExecutor::default(), downloader: None }
48    }
49}
50
51impl<D> PreflightLayer<D>
52where
53    D: StorageDownloader,
54{
55    /// Creates a new [PreflightLayer] with the given executor and downloader.
56    pub fn new(executor: LocalExecutor, downloader: Option<D>) -> Self {
57        Self { executor, downloader }
58    }
59
60    /// Get a clone of the executor used by this layer.
61    pub fn executor_cloned(&self) -> LocalExecutor {
62        self.executor.clone()
63    }
64
65    async fn fetch_env(&self, input: &RequestInput) -> anyhow::Result<GuestEnv> {
66        let env = match input.inputType {
67            RequestInputType::Inline => GuestEnv::decode(&input.data)?,
68            RequestInputType::Url => {
69                let downloader = self
70                    .downloader
71                    .as_ref()
72                    .context("cannot preflight URL input without downloader")?;
73                let input_url =
74                    std::str::from_utf8(&input.data).context("Input URL is not valid UTF-8")?;
75                tracing::info!("Fetching input from {}", input_url);
76                GuestEnv::decode(&downloader.download(input_url).await?)?
77            }
78            _ => bail!("Unsupported input type"),
79        };
80        Ok(env)
81    }
82
83    /// Ensures image_id is set, computing from program (inline or fetched) if needed.
84    async fn ensure_image_id(&self, params: RequestParams) -> anyhow::Result<RequestParams> {
85        if params.image_id.is_some() {
86            return Ok(params);
87        }
88        let program = match params.require_program() {
89            Ok(bytes) => bytes.to_vec(),
90            Err(_) => {
91                let url = params.require_program_url()?;
92                let downloader = self
93                    .downloader
94                    .as_ref()
95                    .context("cannot fetch program URL without downloader")?;
96                downloader.download(url.as_str()).await?
97            }
98        };
99        let image_id = risc0_zkvm::compute_image_id(&program)?;
100        Ok(params.with_image_id(image_id))
101    }
102
103    /// Best-effort: fills executor cache when we have all precomputed data.
104    async fn fill_executor_cache_if_ready(&self, params: &RequestParams) {
105        let (Some(image_id), Some(request_input), Some(cycles), Some(journal)) = (
106            params.image_id,
107            params.request_input.as_ref(),
108            params.cycles,
109            params.journal.as_ref(),
110        ) else {
111            return;
112        };
113        let Ok(env) = self.fetch_env(request_input).await else {
114            return;
115        };
116        tracing::debug!("Filling executor cache for {image_id} with {cycles} cycles");
117        self.executor
118            .insert_execution_data(&image_id.to_string(), &env.stdin, cycles, journal.bytes.clone())
119            .await;
120    }
121}
122
123impl<D> Adapt<PreflightLayer<D>> for RequestParams
124where
125    D: StorageDownloader,
126{
127    type Output = RequestParams;
128    type Error = anyhow::Error;
129
130    async fn process_with(
131        mut self,
132        layer: &PreflightLayer<D>,
133    ) -> Result<Self::Output, Self::Error> {
134        if self.cycles.is_some() && self.journal.is_some() {
135            self = layer.ensure_image_id(self).await?;
136            layer.fill_executor_cache_if_ready(&self).await;
137            return Ok(self);
138        }
139
140        tracing::trace!("Processing {self:?} with PreflightLayer");
141
142        let program_url = self.require_program_url().context("failed to preflight request")?;
143        let request_input = self.require_request_input().context("failed to preflight request")?;
144
145        // Fetch program and input
146        let downloader =
147            layer.downloader.as_ref().context("cannot preflight URL request without downloader")?;
148        let program = downloader.download(program_url.as_str()).await?;
149        let env = layer.fetch_env(request_input).await?;
150        // Use env.stdin directly - this matches what the pricing logic uses for hashing
151        let input_bytes = env.stdin;
152
153        // Compute image_id from the program
154        let image_id = risc0_zkvm::compute_image_id(&program)?;
155        let image_id_str = image_id.to_string();
156
157        // Execute using LocalExecutor (with deduplication)
158        let (stats, journal) = layer
159            .executor
160            .execute_program(&image_id_str, &program, &input_bytes)
161            .await
162            .map_err(|e| anyhow::anyhow!("preflight execution failed: {}", e))?;
163
164        let cycles = stats.total_cycles;
165        let journal = risc0_zkvm::Journal::new(journal);
166
167        // Verify image_id if one was provided
168        if let Some(provided_image_id) = self.image_id {
169            ensure!(
170                provided_image_id == image_id,
171                "provided image ID does not match computed value: {provided_image_id} != {image_id}"
172            );
173        }
174
175        Ok(self.with_cycles(cycles).with_journal(journal).with_image_id(image_id))
176    }
177}
178
179impl Adapt<PreflightLayer<NotProvided>> for RequestParams {
180    type Output = RequestParams;
181    type Error = anyhow::Error;
182
183    async fn process_with(
184        self,
185        _: &PreflightLayer<NotProvided>,
186    ) -> Result<Self::Output, Self::Error> {
187        if self.cycles.is_some() && self.journal.is_some() {
188            return Ok(self);
189        }
190
191        bail!("cannot preflight program without downloader")
192    }
193}