Skip to main content

boundless_market/request_builder/
storage_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, Layer, RequestParams};
16use crate::{
17    contracts::RequestInput, input::GuestEnv, storage::StorageUploader, util::NotProvided,
18    StandardUploader,
19};
20use anyhow::{bail, Context};
21use derive_builder::Builder;
22use url::Url;
23
24/// Configuration for the [StorageLayer].
25///
26/// Controls how programs and inputs are handled during request building.
27#[non_exhaustive]
28#[derive(Clone, Builder)]
29pub struct StorageLayerConfig {
30    /// Maximum number of bytes to send as an inline input.
31    ///
32    /// Inputs larger than this size will be uploaded using the given storage uploader. Set to none
33    /// to indicate that inputs should always be sent inline.
34    #[builder(setter(into), default = "Some(2048)")]
35    pub inline_input_max_bytes: Option<usize>,
36}
37
38/// A layer responsible for storing programs and inputs.
39///
40/// This layer handles the preparation of program and input data for the proof request.
41/// It can upload large programs and inputs to external storage, or include smaller
42/// inputs directly in the request as inline data.
43#[non_exhaustive]
44#[derive(Clone)]
45pub struct StorageLayer<U = StandardUploader> {
46    /// [StorageUploader] used to upload programs and inputs.
47    ///
48    /// If not provided, the layer cannot upload files and provided inputs must be no larger than
49    /// [StorageLayerConfig::inline_input_max_bytes].
50    pub uploader: Option<U>,
51
52    /// Configuration controlling storage behavior.
53    pub config: StorageLayerConfig,
54}
55
56impl StorageLayerConfig {
57    /// Creates a new builder for constructing a [StorageLayerConfig].
58    ///
59    /// This provides a way to customize storage behavior, such as
60    /// the maximum size for inline inputs.
61    pub fn builder() -> StorageLayerConfigBuilder {
62        Default::default()
63    }
64}
65
66impl<S: Clone> From<Option<S>> for StorageLayer<S> {
67    /// Creates a [StorageLayer] from the given [StandardUploader], using default values for all
68    /// other fields.
69    ///
70    /// Provided value is an [Option] such that whether the storage uploader is available can be
71    /// reolved at runtime (e.g. from environment variables).
72    fn from(uploader: Option<S>) -> Self {
73        Self { uploader, config: Default::default() }
74    }
75}
76
77impl<S> From<StorageLayerConfig> for StorageLayer<S>
78where
79    S: StorageUploader + Default,
80{
81    fn from(config: StorageLayerConfig) -> Self {
82        Self { uploader: Some(Default::default()), config }
83    }
84}
85
86impl<S> Default for StorageLayer<S> {
87    fn default() -> Self {
88        Self { uploader: None, config: Default::default() }
89    }
90}
91
92impl From<StorageLayerConfig> for StorageLayer<NotProvided> {
93    fn from(config: StorageLayerConfig) -> Self {
94        Self { uploader: None, config }
95    }
96}
97
98impl Default for StorageLayerConfig {
99    fn default() -> Self {
100        Self::builder().build().expect("implementation error in Default for StorageLayerConfig")
101    }
102}
103
104impl<S> StorageLayer<S>
105where
106    S: StorageUploader,
107{
108    /// Uploads a program binary and returns its URL.
109    ///
110    /// This method requires a configured storage uploader and will return an error
111    /// if none is available.
112    pub async fn process_program(&self, program: &[u8]) -> anyhow::Result<Url> {
113        let storage_uploader = self
114            .uploader
115            .as_ref()
116            .context("cannot upload program using StorageLayer with no storage_uploader")?;
117        let program_url = storage_uploader.upload_program(program).await?;
118        Ok(program_url)
119    }
120
121    /// Processes a guest environment into a [RequestInput].
122    ///
123    /// Small inputs (as determined by configuration) will be included inline in the request.
124    /// Larger inputs will be uploaded to external storage, requiring a configured storage uploader.
125    pub async fn process_env(&self, env: &GuestEnv) -> anyhow::Result<RequestInput> {
126        let input_data = env.encode().context("failed to encode guest environment")?;
127        let request_input = match self.config.inline_input_max_bytes {
128            Some(limit) if input_data.len() > limit => {
129                let storage_uploader = self.uploader.as_ref().with_context( || {
130                    format!("cannot upload input using StorageLayer with no storage_uploader; input length of {} bytes exceeds inline limit of {limit} bytes", input_data.len())
131                })?;
132                RequestInput::url(storage_uploader.upload_input(&input_data).await?)
133            }
134            _ => RequestInput::inline(input_data),
135        };
136        Ok(request_input)
137    }
138}
139
140impl<S> StorageLayer<S> {
141    /// Creates a new [StorageLayer] with the given provider and configuration.
142    ///
143    /// The storage uploader is used to upload programs and inputs to external storage.
144    pub fn new(uploader: Option<S>, config: StorageLayerConfig) -> Self {
145        Self { uploader, config }
146    }
147
148    pub(crate) async fn process_env_no_provider(
149        &self,
150        env: &GuestEnv,
151    ) -> anyhow::Result<RequestInput> {
152        let input_data = env.encode().context("failed to encode guest environment")?;
153        let request_input = match self.config.inline_input_max_bytes {
154            Some(limit) if input_data.len() > limit => {
155                bail!("cannot upload input using StorageLayer with no storage_uploader; input length of {} bytes exceeds inline limit of {limit} bytes", input_data.len());
156            }
157            _ => RequestInput::inline(input_data),
158        };
159        Ok(request_input)
160    }
161}
162
163impl<S> Layer<(&[u8], &GuestEnv)> for StorageLayer<S>
164where
165    S: StorageUploader,
166{
167    type Output = (Url, RequestInput);
168    type Error = anyhow::Error;
169
170    async fn process(
171        &self,
172        (program, env): (&[u8], &GuestEnv),
173    ) -> Result<Self::Output, Self::Error> {
174        let program_url = self.process_program(program).await?;
175        let request_input = self.process_env(env).await?;
176        Ok((program_url, request_input))
177    }
178}
179
180impl Layer<&GuestEnv> for StorageLayer<NotProvided> {
181    type Output = RequestInput;
182    type Error = anyhow::Error;
183
184    async fn process(&self, env: &GuestEnv) -> Result<Self::Output, Self::Error> {
185        let request_input = self.process_env_no_provider(env).await?;
186        Ok(request_input)
187    }
188}
189
190impl<S> Adapt<StorageLayer<S>> for RequestParams
191where
192    S: StorageUploader,
193{
194    type Output = RequestParams;
195    type Error = anyhow::Error;
196
197    async fn process_with(self, layer: &StorageLayer<S>) -> Result<Self::Output, Self::Error> {
198        tracing::trace!("Processing {self:?} with StorageLayer");
199
200        let mut params = self;
201        if params.program_url.is_none() {
202            let program_url = layer.process_program(params.require_program()?).await?;
203            params = params.with_program_url(program_url)?;
204        }
205        if params.request_input.is_none() {
206            let input = layer.process_env(params.require_env()?).await?;
207            params = params.with_request_input(input);
208        }
209        Ok(params)
210    }
211}
212
213impl Adapt<StorageLayer<NotProvided>> for RequestParams {
214    type Output = RequestParams;
215    type Error = anyhow::Error;
216
217    async fn process_with(
218        self,
219        layer: &StorageLayer<NotProvided>,
220    ) -> Result<Self::Output, Self::Error> {
221        tracing::trace!("Processing {self:?} with StorageLayer");
222
223        let mut params = self;
224        params
225            .require_program_url()
226            .context("program_url must be set when storage uploader is not provided")?;
227        if params.request_input.is_none() {
228            let input = layer.process_env_no_provider(params.require_env()?).await?;
229            params = params.with_request_input(input);
230        }
231        Ok(params)
232    }
233}