Skip to main content

boundless_market/request_builder/
mod.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 std::{
16    borrow::Cow,
17    fmt,
18    fmt::{Debug, Display},
19    future::Future,
20};
21
22use alloy::{
23    network::Ethereum,
24    providers::{DynProvider, Provider},
25};
26use derive_builder::Builder;
27use risc0_zkvm::{Digest, Journal};
28use url::Url;
29
30use crate::{
31    contracts::{ProofRequest, RequestId, RequestInput},
32    input::GuestEnv,
33    prover_utils::requestor_pricing::requestor_order_preflight,
34    selector::SelectorExt,
35    storage::{StandardDownloader, StorageDownloader},
36    util::{now_timestamp, NotProvided},
37    StandardUploader,
38};
39mod preflight_layer;
40mod storage_layer;
41
42pub use preflight_layer::PreflightLayer;
43pub use storage_layer::{StorageLayer, StorageLayerConfig, StorageLayerConfigBuilder};
44mod requirements_layer;
45pub use requirements_layer::{RequirementParams, RequirementsLayer};
46mod request_id_layer;
47pub use request_id_layer::{
48    RequestIdLayer, RequestIdLayerConfig, RequestIdLayerConfigBuilder, RequestIdLayerMode,
49};
50mod offer_layer;
51pub use offer_layer::{
52    OfferLayer, OfferLayerConfig, OfferLayerConfigBuilder, OfferParams, OfferParamsBuilder,
53};
54mod finalizer;
55pub use finalizer::{Finalizer, FinalizerConfig, FinalizerConfigBuilder};
56
57/// A trait for building proof requests, used by the [Client][crate::Client].
58///
59/// See [StandardRequestBuilder] for an example implementation.
60pub trait RequestBuilder<Params> {
61    /// Error type that may be returned by this builder.
62    type Error;
63
64    // NOTE: Takes the self receiver so that the caller does not need to explicitly name the
65    // RequestBuilder trait (e.g. `<MyRequestBuilder as RequestBuilder>::params()`). This could
66    // also be used to set initial values on the params that are specific to the rrequest builder.
67    /// Returns a default instance of the parameter type used by this builder.
68    fn params(&self) -> Params
69    where
70        Params: Default,
71    {
72        Default::default()
73    }
74
75    /// Builds a [ProofRequest] using the provided parameters.
76    fn build(
77        &self,
78        params: impl Into<Params>,
79    ) -> impl Future<Output = Result<ProofRequest, Self::Error>>;
80}
81
82/// Blanket implementation for [RequestBuilder] for all [Layer] that output a proof request.
83///
84/// This implementation allows for custom and modified layered builders to automatically be usable
85/// as a [RequestBuilder].
86impl<L, Params> RequestBuilder<Params> for L
87where
88    L: Layer<Params, Output = ProofRequest>,
89{
90    type Error = L::Error;
91
92    async fn build(&self, params: impl Into<Params>) -> Result<ProofRequest, Self::Error> {
93        self.process(params.into()).await
94    }
95}
96
97/// A trait representing a processing layer in a request building pipeline.
98///
99/// Layers can be composed together to form a multi-step processing pipeline where the output
100/// of one layer becomes the input to the next. Each layer handles a specific aspect of the
101/// request building process.
102pub trait Layer<Input> {
103    /// The output type produced by this layer.
104    type Output;
105
106    /// Error type that may be returned by this layer.
107    type Error;
108
109    /// Processes the input and returns the transformed output.
110    fn process(&self, input: Input) -> impl Future<Output = Result<Self::Output, Self::Error>>;
111}
112
113/// A trait for adapting types to be processed by a [Layer].
114///
115/// This trait provides a mechanism for a type to be processed by a layer, enabling
116/// the composition of multiple layers into a processing pipeline. Inputs are adapted
117/// to work with specific layer types, with the output of one layer feeding into the next.
118///
119/// Existing [Layer] implementations can be adapted to work with new parameter types by
120/// implementating `Adapt<Layer>` on the parameter type.
121pub trait Adapt<L> {
122    /// The output type after processing by the layer.
123    type Output;
124
125    /// Error type that may be returned during processing.
126    type Error;
127
128    /// Processes this value with the provided layer.
129    fn process_with(self, layer: &L) -> impl Future<Output = Result<Self::Output, Self::Error>>;
130}
131
132impl<L, I> Adapt<L> for I
133where
134    L: Layer<I>,
135{
136    type Output = L::Output;
137    type Error = L::Error;
138
139    async fn process_with(self, layer: &L) -> Result<Self::Output, Self::Error> {
140        layer.process(self).await
141    }
142}
143
144/// Define a layer as a stack of two layers. Output of layer A is piped into layer B.
145impl<A, B, Input> Layer<Input> for (A, B)
146where
147    Input: Adapt<A>,
148    <Input as Adapt<A>>::Output: Adapt<B>,
149    <Input as Adapt<A>>::Error: Into<<<Input as Adapt<A>>::Output as Adapt<B>>::Error>,
150{
151    type Output = <<Input as Adapt<A>>::Output as Adapt<B>>::Output;
152    type Error = <<Input as Adapt<A>>::Output as Adapt<B>>::Error;
153
154    async fn process(&self, input: Input) -> Result<Self::Output, Self::Error> {
155        input.process_with(&self.0).await.map_err(Into::into)?.process_with(&self.1).await
156    }
157}
158
159/// A standard implementation of [RequestBuilder] that uses a layered architecture.
160///
161/// This builder composes multiple layers, each handling a specific aspect of request building:
162/// - `storage_layer`: Manages program and input storage
163/// - `preflight_layer`: Validates and simulates the request
164/// - `requirements_layer`: Sets up verification requirements
165/// - `request_id_layer`: Manages request identifier generation
166/// - `offer_layer`: Configures the offer details
167/// - `finalizer`: Validates and finalizes the request
168///
169/// Each layer processes the request in sequence, with the output of one layer becoming
170/// the input for the next.
171#[derive(Clone, Builder)]
172#[non_exhaustive]
173pub struct StandardRequestBuilder<P = DynProvider, U = StandardUploader, D = StandardDownloader> {
174    /// Handles uploading and preparing program and input data.
175    #[builder(setter(into), default)]
176    pub storage_layer: StorageLayer<U>,
177
178    /// Executes preflight checks to validate the request.
179    #[builder(setter(into), default)]
180    pub preflight_layer: PreflightLayer<D>,
181
182    /// Configures the requirements for the proof request.
183    #[builder(setter(into), default)]
184    pub requirements_layer: RequirementsLayer,
185
186    /// Generates and manages request identifiers.
187    #[builder(setter(into))]
188    pub request_id_layer: RequestIdLayer<P>,
189
190    /// Configures offer parameters for the request.
191    #[builder(setter(into))]
192    pub offer_layer: OfferLayer<P>,
193
194    /// Finalizes and validates the complete request.
195    #[builder(setter(into), default)]
196    pub finalizer: Finalizer,
197
198    /// Whether to skip preflight/pricing checks.
199    ///
200    /// If `Some(true)`, preflight checks are skipped.
201    /// If `Some(false)`, preflight checks are run.
202    /// If `None`, falls back to checking the `BOUNDLESS_IGNORE_PREFLIGHT` environment variable.
203    #[builder(setter(into, strip_option), default)]
204    pub skip_preflight: Option<bool>,
205}
206
207impl StandardRequestBuilder<NotProvided, NotProvided, NotProvided> {
208    /// Creates a new builder for constructing a [StandardRequestBuilder].
209    ///
210    /// This is the entry point for creating a request builder with specific
211    /// provider and storage implementations.
212    ///
213    /// # Type Parameters
214    /// * `P` - An Ethereum RPC provider, using alloy.
215    /// * `S` - The storage uploader type for storing programs and inputs.
216    /// * `D` - The storage downloader type for fetching programs and inputs during preflight.
217    pub fn builder<P: Clone, S: Clone, D: Clone>() -> StandardRequestBuilderBuilder<P, S, D> {
218        Default::default()
219    }
220}
221
222impl<P, S, D> StandardRequestBuilder<P, S, D>
223where
224    P: Provider<Ethereum> + 'static + Clone,
225    D: StorageDownloader,
226{
227    fn should_skip_preflight(&self) -> bool {
228        self.skip_preflight.unwrap_or_else(|| std::env::var("BOUNDLESS_IGNORE_PREFLIGHT").is_ok())
229    }
230
231    async fn run_pricing_check(&self, request: &ProofRequest) {
232        if self.should_skip_preflight() {
233            return;
234        }
235
236        let market_address = *self.request_id_layer.boundless_market.instance().address();
237        let chain_id = match self.request_id_layer.boundless_market.get_chain_id().await {
238            Ok(id) => id,
239            Err(e) => {
240                tracing::error!("Failed to get chain ID for pricing check: {:#}", e);
241                return;
242            }
243        };
244        let provider = std::sync::Arc::new(self.offer_layer.provider.clone());
245        let price_provider = self.offer_layer.price_provider.clone();
246
247        if let Err(e) = requestor_order_preflight(
248            request.clone(),
249            self.preflight_layer.executor_cloned(),
250            provider,
251            market_address,
252            chain_id,
253            price_provider,
254        )
255        .await
256        {
257            tracing::error!("Pricing check failed: {:#}", e);
258        }
259    }
260
261    async fn build_from_params(
262        &self,
263        params: RequestParams,
264    ) -> Result<ProofRequest, anyhow::Error> {
265        let params = params
266            .process_with(&self.preflight_layer)
267            .await?
268            .process_with(&self.requirements_layer)
269            .await?
270            .process_with(&self.request_id_layer)
271            .await?
272            .process_with(&self.offer_layer)
273            .await?;
274
275        let request = params.process_with(&self.finalizer).await?;
276        self.run_pricing_check(&request).await;
277        Ok(request)
278    }
279}
280
281impl<P> StandardRequestBuilder<P, NotProvided>
282where
283    P: Provider<Ethereum> + 'static + Clone,
284{
285    /// Build a proof request.
286    ///
287    /// This method also runs a pricing check to validate the request against the same
288    /// pricing logic that provers use, giving early feedback on whether the request
289    /// will be accepted. Pricing checks can be skipped by calling `skip_preflight(true)`
290    /// on the builder, or by setting the `BOUNDLESS_IGNORE_PREFLIGHT` environment variable.
291    pub async fn build(
292        &self,
293        input: impl Into<RequestParams>,
294    ) -> Result<ProofRequest, anyhow::Error> {
295        let params = input.into().process_with(&self.storage_layer).await?;
296        self.build_from_params(params).await
297    }
298}
299
300impl<P, S, D> Layer<RequestParams> for StandardRequestBuilder<P, S, D>
301where
302    P: Provider<Ethereum> + 'static + Clone,
303    D: StorageDownloader,
304    RequestParams: Adapt<StorageLayer<S>, Output = RequestParams, Error = anyhow::Error>,
305    RequestParams: Adapt<PreflightLayer<D>, Output = RequestParams, Error = anyhow::Error>,
306{
307    type Output = ProofRequest;
308    type Error = anyhow::Error;
309
310    async fn process(&self, input: RequestParams) -> Result<ProofRequest, Self::Error> {
311        let params = input.process_with(&self.storage_layer).await?;
312        self.build_from_params(params).await
313    }
314}
315
316// NOTE: We don't use derive_builder here because we need to be able to access the values on the
317// incrementally built parameters.
318/// Parameters for building a proof request.
319///
320/// This struct holds all the necessary information for constructing a [ProofRequest].
321/// It provides a builder pattern for incrementally setting fields and methods for
322/// validating and accessing the parameters.
323///
324/// Most fields are optional and can be populated during the request building process
325/// by various layers. The structure serves as the central data container that passes
326/// through the request builder pipeline.
327#[non_exhaustive]
328#[derive(Clone, Default)]
329pub struct RequestParams {
330    /// RISC-V guest program that will be run in the zkVM.
331    pub program: Option<Cow<'static, [u8]>>,
332
333    /// Guest execution environment, providing the input for the guest.
334    /// See [GuestEnv].
335    pub env: Option<GuestEnv>,
336
337    /// Uploaded program URL, from which provers will fetch the program.
338    pub program_url: Option<Url>,
339
340    /// Prepared input for the [ProofRequest], containing either a URL or inline input.
341    /// See [RequestInput].
342    pub request_input: Option<RequestInput>,
343
344    /// Count of the RISC Zero execution cycles. Used to estimate proving cost.
345    pub cycles: Option<u64>,
346
347    /// Image ID identifying the program being executed.
348    pub image_id: Option<Digest>,
349
350    /// Contents of the [Journal] that results from the execution.
351    pub journal: Option<Journal>,
352
353    /// [RequestId] to use for the proof request.
354    pub request_id: Option<RequestId>,
355
356    /// [OfferParams] for constructing the [Offer][crate::Offer] to send along with the request.
357    pub offer: OfferParams,
358
359    /// [RequirementParams] for constructing the [Requirements][crate::Requirements] for the resulting proof.
360    pub requirements: RequirementParams,
361}
362
363impl RequestParams {
364    /// Creates a new empty instance of [RequestParams].
365    ///
366    /// This is equivalent to calling `Default::default()` and is provided as a
367    /// convenience method for better readability when building requests.
368    pub fn new() -> Self {
369        Self::default()
370    }
371
372    /// Gets the program bytes, returning an error if not set.
373    ///
374    /// This method is used by layers in the request building pipeline to access
375    /// the program when it's required for processing.
376    pub fn require_program(&self) -> Result<&[u8], MissingFieldError> {
377        self.program
378            .as_deref()
379            .ok_or(MissingFieldError::with_hint("program", "can be set using .with_program(...)"))
380    }
381
382    /// Sets the program to be executed in the zkVM.
383    pub fn with_program(self, value: impl Into<Cow<'static, [u8]>>) -> Self {
384        Self { program: Some(value.into()), ..self }
385    }
386
387    /// Gets the guest environment, returning an error if not set.
388    ///
389    /// The guest environment contains the input data for the program.
390    pub fn require_env(&self) -> Result<&GuestEnv, MissingFieldError> {
391        self.env.as_ref().ok_or(MissingFieldError::with_hint(
392            "env",
393            "can be set using .with_env(...) or .with_stdin",
394        ))
395    }
396
397    /// Sets the [GuestEnv], providing the guest with input.
398    ///
399    /// Can be constructed with [GuestEnvBuilder][crate::input::GuestEnvBuilder].
400    ///
401    /// ```rust
402    /// # use boundless_market::request_builder::RequestParams;
403    /// # const ECHO_ELF: &[u8] = b"";
404    /// use boundless_market::GuestEnv;
405    ///
406    /// RequestParams::new()
407    ///     .with_program(ECHO_ELF)
408    ///     .with_env(GuestEnv::builder()
409    ///         .write_frame(b"hello!")
410    ///         .write_frame(b"goodbye."));
411    /// ```
412    ///
413    /// See also [Self::with_env] and [GuestEnvBuilder][crate::input::GuestEnvBuilder]
414    pub fn with_env(self, value: impl Into<GuestEnv>) -> Self {
415        Self { env: Some(value.into()), ..self }
416    }
417
418    /// Sets the [GuestEnv] to be contain the given bytes as `stdin`.
419    ///
420    /// Note that the bytes are passed directly to the guest without encoding. If your guest
421    /// expects the input to be encoded in any way (e.g. `bincode`), the caller must encode the
422    /// data before passing it.
423    ///
424    /// If the [GuestEnv] is already set, this replaces it.
425    ///
426    /// ```rust
427    /// # use boundless_market::request_builder::RequestParams;
428    /// # const ECHO_ELF: &[u8] = b"";
429    /// RequestParams::new()
430    ///     .with_program(ECHO_ELF)
431    ///     .with_stdin(b"hello!");
432    /// ```
433    ///
434    /// See also [Self::with_env] and [GuestEnvBuilder][crate::input::GuestEnvBuilder]
435    pub fn with_stdin(self, value: impl Into<Vec<u8>>) -> Self {
436        Self { env: Some(GuestEnv::from_stdin(value)), ..self }
437    }
438
439    /// Gets the program URL, returning an error if not set.
440    ///
441    /// The program URL is where provers will download the program to execute.
442    pub fn require_program_url(&self) -> Result<&Url, MissingFieldError> {
443        self.program_url.as_ref().ok_or(MissingFieldError::with_hint(
444            "program_url",
445            "can be set using .with_program_url(...)",
446        ))
447    }
448
449    /// Set the program URL, where provers can download the program to be proven.
450    ///
451    /// ```rust
452    /// # use boundless_market::request_builder::RequestParams;
453    /// # || -> anyhow::Result<()> {
454    /// RequestParams::new()
455    ///     .with_program_url("https://fileserver.example/guest.bin")?;
456    /// # Ok(())
457    /// # }().unwrap();
458    /// ```
459    pub fn with_program_url<T: TryInto<Url>>(self, value: T) -> Result<Self, T::Error> {
460        Ok(Self { program_url: Some(value.try_into()?), ..self })
461    }
462
463    /// Gets the request input, returning an error if not set.
464    ///
465    /// The request input contains the input data for the guest program, either inline or as a URL.
466    pub fn require_request_input(&self) -> Result<&RequestInput, MissingFieldError> {
467        self.request_input.as_ref().ok_or(MissingFieldError::with_hint(
468            "request_input",
469            "can be set using .with_request_input(...)",
470        ))
471    }
472
473    /// Sets the encoded input data for the request. This data will be decoded by the prover into a
474    /// [GuestEnv] that will be used to run the guest.
475    ///
476    /// If not provided, the this will be constructed from the data given via
477    /// [RequestParams::with_env] or [RequestParams::with_stdin]. If the input is set with both
478    /// this method and one of those two, the input specified here takes precedence.
479    pub fn with_request_input(self, value: impl Into<RequestInput>) -> Self {
480        Self { request_input: Some(value.into()), ..self }
481    }
482
483    /// Sets the input as a URL from which provers can download the input data.
484    ///
485    /// This is a convenience method that creates a [RequestInput] with URL type.
486    ///
487    /// ```rust
488    /// # use boundless_market::request_builder::RequestParams;
489    /// # || -> anyhow::Result<()> {
490    /// RequestParams::new()
491    ///     .with_input_url("https://fileserver.example/input.bin")?;
492    /// # Ok(())
493    /// # }().unwrap();
494    /// ```
495    pub fn with_input_url<T: TryInto<Url>>(self, value: T) -> Result<Self, T::Error> {
496        Ok(Self { request_input: Some(RequestInput::url(value.try_into()?)), ..self })
497    }
498
499    /// Gets the cycle count, returning an error if not set.
500    ///
501    /// The cycle count is used to estimate proving costs.
502    pub fn require_cycles(&self) -> Result<u64, MissingFieldError> {
503        self.cycles
504            .ok_or(MissingFieldError::with_hint("cycles", "can be set using .with_cycles(...)"))
505    }
506
507    /// Sets the cycle count for the proof request.
508    ///
509    /// This is used to estimate proving costs and determine appropriate pricing.
510    pub fn with_cycles(self, value: u64) -> Self {
511        Self { cycles: Some(value), ..self }
512    }
513
514    /// Gets the journal, returning an error if not set.
515    ///
516    /// The journal contains the output from the guest program execution.
517    pub fn require_journal(&self) -> Result<&Journal, MissingFieldError> {
518        self.journal
519            .as_ref()
520            .ok_or(MissingFieldError::with_hint("journal", "can be set using .with_journal(...)"))
521    }
522
523    /// Sets the journal for the request.
524    ///
525    /// The journal is the output from the guest program execution and is used
526    /// to configure verification requirements.
527    pub fn with_journal(self, value: impl Into<Journal>) -> Self {
528        Self { journal: Some(value.into()), ..self }
529    }
530
531    /// Gets the image ID, returning an error if not set.
532    ///
533    /// The image ID uniquely identifies the program being executed.
534    pub fn require_image_id(&self) -> Result<Digest, MissingFieldError> {
535        self.image_id.ok_or(MissingFieldError::with_hint(
536            "image_id",
537            "can be set using .with_image_id(...), and is calculated from the program",
538        ))
539    }
540
541    /// Sets the image ID for the request.
542    ///
543    /// The image ID is the hash of the program binary and uniquely identifies
544    /// the program being executed.
545    pub fn with_image_id(self, value: impl Into<Digest>) -> Self {
546        Self { image_id: Some(value.into()), ..self }
547    }
548
549    /// Gets the request ID, returning an error if not set.
550    ///
551    /// The request ID contains the requestor's address and a unique index,
552    /// and is used to track the request throughout its lifecycle.
553    pub fn require_request_id(&self) -> Result<&RequestId, MissingFieldError> {
554        self.request_id.as_ref().ok_or(MissingFieldError::with_hint("request_id", "can be set using .with_request_id(...), and can be generated by boundless_market::Client"))
555    }
556
557    /// Sets the request ID for the proof request.
558    ///
559    /// The request ID contains the requestor's address and a unique index,
560    /// and is used to track the request throughout its lifecycle.
561    pub fn with_request_id(self, value: impl Into<RequestId>) -> Self {
562        Self { request_id: Some(value.into()), ..self }
563    }
564
565    /// Configure the [Offer][crate::Offer] on the [ProofRequest] by either providing a complete
566    /// offer, or a partial offer via [OfferParams].
567    ///
568    /// ```rust
569    /// # use boundless_market::request_builder::{RequestParams, OfferParams};
570    /// use alloy::primitives::utils::parse_units;
571    ///
572    /// RequestParams::new()
573    ///     .with_offer(OfferParams::builder()
574    ///         .max_price(parse_units("0.01", "ether").unwrap())
575    ///         .ramp_up_period(30)
576    ///         .lock_timeout(120)
577    ///         .timeout(240));
578    /// ```
579    pub fn with_offer(self, value: impl Into<OfferParams>) -> Self {
580        Self { offer: value.into(), ..self }
581    }
582
583    /// Configure the [Requirements][crate::Requirements] on the [ProofRequest] by either providing
584    /// the complete requirements, or partial requirements via [RequirementParams].
585    ///
586    /// ```rust
587    /// # use boundless_market::request_builder::{RequestParams, RequirementParams};
588    /// use alloy::primitives::address;
589    ///
590    /// RequestParams::new()
591    ///     .with_requirements(RequirementParams::builder()
592    ///         .callback_address(address!("0x00000000000000000000000000000000deadbeef")));
593    /// ```
594    pub fn with_requirements(self, value: impl Into<RequirementParams>) -> Self {
595        Self { requirements: value.into(), ..self }
596    }
597
598    /// Request a stand-alone Groth16 proof for this request.
599    ///
600    /// This is a convinience method to set the selector on the requirements. Note that calling
601    /// [RequestParams::with_requirements] after this function will overwrite the change.
602    pub fn with_groth16_proof(self) -> Self {
603        let mut requirements = self.requirements;
604        requirements.selector = match crate::util::is_dev_mode() {
605            true => Some((SelectorExt::FakeReceipt as u32).into()),
606            false => Some((SelectorExt::groth16_latest() as u32).into()),
607        };
608        Self { requirements, ..self }
609    }
610
611    /// Request a stand-alone Blake3 Groth16 proof for this request.
612    ///
613    /// This is a convinience method to set the selector on the requirements. Note that calling
614    /// [RequestParams::with_requirements] after this function will overwrite the change.
615    pub fn with_blake3_groth16_proof(self) -> Self {
616        let mut requirements = self.requirements;
617        requirements.selector = match crate::util::is_dev_mode() {
618            true => Some((SelectorExt::FakeReceipt as u32).into()),
619            false => Some((SelectorExt::blake3_groth16_latest() as u32).into()),
620        };
621        // TODO(ec2): should we automatically set the predicate type to claim digest match here?
622        Self { requirements, ..self }
623    }
624}
625
626impl Debug for RequestParams {
627    /// [Debug] implementation that does not print the contents of the program.
628    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
629        f.debug_struct("ExampleRequestParams")
630            .field("program", &self.program.as_ref().map(|x| format!("[{} bytes]", x.len())))
631            .field("env", &self.env)
632            .field("program_url", &self.program_url)
633            .field("input", &self.request_input)
634            .field("cycles", &self.cycles)
635            .field("journal", &self.journal)
636            .field("request_id", &self.request_id)
637            .field("offer", &self.offer)
638            .field("requirements", &self.requirements)
639            .finish()
640    }
641}
642
643impl<Program, Env> From<(Program, Env)> for RequestParams
644where
645    Program: Into<Cow<'static, [u8]>>,
646    Env: Into<GuestEnv>,
647{
648    fn from(value: (Program, Env)) -> Self {
649        Self::default().with_program(value.0).with_env(value.1)
650    }
651}
652
653/// Error indicating that a required field is missing when building a request.
654///
655/// This error is returned when attempting to access a field that hasn't been
656/// set yet in the request parameters.
657#[derive(Debug)]
658pub struct MissingFieldError {
659    /// The name of the missing field.
660    pub label: Cow<'static, str>,
661    /// An optional hint as to the cause of the error, or how to resolve it.
662    pub hint: Option<Cow<'static, str>>,
663}
664
665impl Display for MissingFieldError {
666    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667        match self.hint {
668            None => write!(f, "field `{}` is required but is uninitialized", self.label),
669            Some(ref hint) => {
670                write!(f, "field `{}` is required but is uninitialized; {hint}", self.label)
671            }
672        }
673    }
674}
675
676impl std::error::Error for MissingFieldError {}
677
678impl MissingFieldError {
679    /// Creates a new error for the specified missing field.
680    pub fn new(label: impl Into<Cow<'static, str>>) -> Self {
681        Self { label: label.into(), hint: None }
682    }
683
684    /// Creates a new error for the specified missing field.
685    pub fn with_hint(
686        label: impl Into<Cow<'static, str>>,
687        hint: impl Into<Cow<'static, str>>,
688    ) -> Self {
689        Self { label: label.into(), hint: Some(hint.into()) }
690    }
691}
692
693/// Parameterization mode for the request builder.
694///
695/// Defines the proving and executor speeds used to calculate recommended timeouts.
696/// Note: setting faster speeds may result in fewer provers being able to fulfill the request,
697/// higher prices, and lower fulfillment guarantees.
698#[derive(Debug, Clone, Copy, PartialEq)]
699#[non_exhaustive]
700pub struct ParameterizationMode {
701    /// Executor speed in Hz.
702    executor_speed: u64,
703    /// Minimum timeout in seconds.
704    base_timeout: u32,
705    /// Base ramp up delay in seconds.
706    base_ramp_up_delay: u64,
707    /// Base ramp up period in seconds.
708    base_ramp_up_period: u32,
709    /// Timeout seconds per mcycle.
710    timeout_seconds_per_mcycle: u32,
711    /// Ramp up seconds per mcycle.
712    ramp_up_seconds_per_mcycle: u32,
713}
714
715impl ParameterizationMode {
716    /// Default executor speed in Hz.
717    const DEFAULT_EXECUTOR_SPEED_HZ: u64 = 1000000; // 1 MHz
718
719    /// Minimum default timeout in seconds.
720    ///
721    /// This is to prevent the timeout from being too short and causing the request
722    /// to expire before the prover can execute and evaluate the request.
723    const DEFAULT_BASE_TIMEOUT: u32 = 1800; // 30 minutes
724
725    /// Default timeout seconds per mcycle.
726    ///
727    /// This is used to calculate the timeout based on the cycle count.
728    const DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE: u32 = 7;
729
730    /// Default ramp up seconds per mcycle.
731    ///
732    /// This is used to calculate the ramp up period based on the cycle count.
733    const DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE: u32 = 2;
734
735    /// Default base ramp up period in seconds.
736    ///
737    /// This is used to ensure that the ramp up period is long enough
738    /// to allow provers to execute and evaluate the request.
739    const DEFAULT_BASE_RAMP_UP_PERIOD: u32 = 600; // 10 minutes
740
741    /// Default minimum ramp up delay in seconds.
742    ///
743    /// This is used to ensure that the ramp up start is set
744    /// to allow provers to execute and evaluate the request.
745    const DEFAULT_BASE_RAMP_UP_DELAY: u64 = 30;
746
747    /// Creates a parameterization mode for fulfillment.
748    ///
749    /// This mode is more conservative and ensures more provers can fulfill the request.
750    ///
751    /// Sets the ramp up period as 10x the executor time assuming the executor speed is 30 MHz.
752    /// Sets the lock timeout as the sum of the ramp up period and the proving and executor times
753    /// assuming the proving speed is 500 kHz and the executor speed is 30 MHz and capped at 4 hours.
754    /// Sets the timeout as 2 times the lock timeout and capped at 8 hours.
755    pub fn fulfillment() -> Self {
756        Self {
757            executor_speed: Self::DEFAULT_EXECUTOR_SPEED_HZ,
758            base_timeout: Self::DEFAULT_BASE_TIMEOUT,
759            timeout_seconds_per_mcycle: Self::DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE,
760            ramp_up_seconds_per_mcycle: Self::DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE,
761            base_ramp_up_delay: Self::DEFAULT_BASE_RAMP_UP_DELAY,
762            base_ramp_up_period: Self::DEFAULT_BASE_RAMP_UP_PERIOD,
763        }
764    }
765
766    /// Calculates the recommended ramp up start based on the cycle count and speeds.
767    ///
768    /// The ramp up start is calculated as the current timestamp plus the base ramp up delay
769    /// or the required executor time, whichever is greater.
770    fn recommended_ramp_up_start(&self, cycle_count: Option<u64>) -> u64 {
771        cycle_count
772            .filter(|&count| count > 0)
773            .map(|cycle_count| {
774                now_timestamp()
775                    + self.base_ramp_up_delay.max(self.executor_time(Some(cycle_count)) as u64)
776            })
777            .unwrap_or(now_timestamp() + self.base_ramp_up_delay)
778    }
779
780    /// Calculates the recommended ramp up period based on the cycle count.
781    ///
782    /// The ramp up period is calculated as the number of mcycles multiplied by the ramp up seconds per mcycle plus the base ramp up period.
783    /// The ramp up period is capped at 2 hours to prevent the ramp up period from being too long.
784    fn recommended_ramp_up_period(&self, cycle_count: Option<u64>) -> u32 {
785        const MAX_RAMP_UP_PERIOD: u32 = 7200; // 2 hours
786        cycle_count
787            .filter(|&count| count > 0)
788            .map(|cycle_count| {
789                let m_cycles = cycle_count.div_ceil(1_000_000) as u32;
790                m_cycles
791                    .saturating_mul(self.ramp_up_seconds_per_mcycle)
792                    .saturating_add(self.base_ramp_up_period)
793                    .min(MAX_RAMP_UP_PERIOD)
794            })
795            .unwrap_or(self.base_ramp_up_period)
796    }
797
798    /// Calculates the recommended ramp up delay based on the cycle count and speeds.
799    /// Calculates the recommended timeout based on the cycle count and speeds.
800    ///
801    /// The timeout is calculated as the sum of:
802    /// - Time required for proving: `cycle_count / proving_speed`
803    /// - Time required for execution: `cycle_count / executor_speed`
804    ///
805    /// # Notes
806    /// The timeout is guaranteed to be at least [self.min_timeout] seconds.
807    fn recommended_timeout(&self, cycle_count: Option<u64>) -> u32 {
808        const MAX_TIMEOUT: u32 = 14400; // 4 hours
809        cycle_count
810            .filter(|&count| count > 0)
811            .map(|cycle_count| {
812                let m_cycles = cycle_count.div_ceil(1_000_000) as u32;
813                m_cycles
814                    .saturating_mul(self.timeout_seconds_per_mcycle)
815                    .saturating_add(self.base_timeout)
816                    .min(MAX_TIMEOUT)
817            })
818            .unwrap_or(self.base_timeout)
819    }
820
821    /// Calculates the required executor time based on the cycle count and speeds.
822    ///
823    /// The executor time is calculated as the cycle count divided by the executor speed.
824    fn executor_time(&self, cycle_count: Option<u64>) -> u32 {
825        cycle_count
826            .filter(|&count| count > 0)
827            .map(|cycle_count| cycle_count.div_ceil(self.executor_speed) as u32)
828            .unwrap_or(0)
829    }
830}
831
832impl Default for ParameterizationMode {
833    fn default() -> Self {
834        Self {
835            executor_speed: Self::DEFAULT_EXECUTOR_SPEED_HZ,
836            base_timeout: Self::DEFAULT_BASE_TIMEOUT,
837            timeout_seconds_per_mcycle: Self::DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE,
838            ramp_up_seconds_per_mcycle: Self::DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE,
839            base_ramp_up_delay: Self::DEFAULT_BASE_RAMP_UP_DELAY,
840            base_ramp_up_period: Self::DEFAULT_BASE_RAMP_UP_PERIOD,
841        }
842    }
843}
844
845#[cfg(test)]
846mod parameterization_mode_tests {
847    use super::ParameterizationMode;
848
849    #[test]
850    fn test_default_creation() {
851        let mode = ParameterizationMode::default();
852        assert_eq!(mode.executor_speed, ParameterizationMode::DEFAULT_EXECUTOR_SPEED_HZ);
853        assert_eq!(mode.base_timeout, ParameterizationMode::DEFAULT_BASE_TIMEOUT);
854        assert_eq!(mode.base_ramp_up_delay, ParameterizationMode::DEFAULT_BASE_RAMP_UP_DELAY);
855        assert_eq!(mode.base_ramp_up_period, ParameterizationMode::DEFAULT_BASE_RAMP_UP_PERIOD);
856        assert_eq!(
857            mode.timeout_seconds_per_mcycle,
858            ParameterizationMode::DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE
859        );
860        assert_eq!(
861            mode.ramp_up_seconds_per_mcycle,
862            ParameterizationMode::DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE
863        );
864    }
865
866    #[test]
867    fn test_fulfillment_creation() {
868        let mode = ParameterizationMode::fulfillment();
869        assert_eq!(mode.executor_speed, ParameterizationMode::DEFAULT_EXECUTOR_SPEED_HZ);
870        assert_eq!(mode.base_timeout, ParameterizationMode::DEFAULT_BASE_TIMEOUT);
871        assert_eq!(mode.base_ramp_up_delay, ParameterizationMode::DEFAULT_BASE_RAMP_UP_DELAY);
872        assert_eq!(mode.base_ramp_up_period, ParameterizationMode::DEFAULT_BASE_RAMP_UP_PERIOD);
873        assert_eq!(
874            mode.timeout_seconds_per_mcycle,
875            ParameterizationMode::DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE
876        );
877        assert_eq!(
878            mode.ramp_up_seconds_per_mcycle,
879            ParameterizationMode::DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE
880        );
881    }
882
883    #[test]
884    fn test_recommended_timeout_default() {
885        let mode = ParameterizationMode::default();
886
887        // Test with a small cycle count
888        let cycle_count = 1_000_000; // 1M cycles
889        let timeout = mode.recommended_timeout(Some(cycle_count));
890
891        // Expected: 1_000_000 / 1_000_000 * 7 + 1800 = 7 + 1800 = 1807 seconds
892        assert_eq!(timeout, 1807);
893
894        // Test with a larger cycle count that exceeds MIN_TIMEOUT
895        let cycle_count = 50_000_000; // 50M cycles
896        let timeout = mode.recommended_timeout(Some(cycle_count));
897
898        // Expected: 50_000_000 / 1_000_000 * 7 + 1800 = 350 + 1800 = 2150 seconds
899        assert_eq!(timeout, 2150);
900    }
901
902    #[test]
903    fn test_recommended_timeout_minimum() {
904        let mode = ParameterizationMode::default();
905
906        // Test with zero cycles - should return DEFAULT_TIMEOUT
907        let timeout = mode.recommended_timeout(Some(0));
908        assert_eq!(timeout, mode.base_timeout);
909
910        // Test with very small cycle count - should return base timeout
911        let timeout = mode.recommended_timeout(Some(100));
912        assert_eq!(timeout, mode.base_timeout + mode.timeout_seconds_per_mcycle);
913    }
914
915    #[test]
916    fn test_recommended_ramp_up_period_default() {
917        let mode = ParameterizationMode::default();
918
919        // Test with 1M cycles
920        let cycle_count = 1_000_000;
921        let ramp_up = mode.recommended_ramp_up_period(Some(cycle_count as u64));
922
923        // Expected: 1_000_000 / 1_000_000 * 2 + 600 = 2 + 600 = 602 seconds
924        assert_eq!(ramp_up, 602);
925
926        // Test with 50M cycles
927        let cycle_count = 50_000_000;
928        let ramp_up = mode.recommended_ramp_up_period(Some(cycle_count as u64));
929
930        // Expected: 50_000_000 / 1_000_000 * 2 + 600 = 100 + 600 = 700 seconds
931        assert_eq!(ramp_up, 700);
932    }
933
934    #[test]
935    fn test_recommended_ramp_up_period_zero_cycles() {
936        let mode = ParameterizationMode::default();
937
938        // Test with zero cycles
939        let ramp_up = mode.recommended_ramp_up_period(Some(0));
940        assert_eq!(ramp_up, ParameterizationMode::DEFAULT_BASE_RAMP_UP_PERIOD);
941    }
942
943    #[test]
944    fn test_recommended_ramp_up_start_default() {
945        let mode = ParameterizationMode::default();
946        let now = crate::util::now_timestamp();
947
948        // Test with zero cycles - should return now + base ramp up delay
949        let ramp_up_start = mode.recommended_ramp_up_start(Some(0));
950        assert!(ramp_up_start >= now + mode.base_ramp_up_delay);
951        assert!(ramp_up_start <= now + mode.base_ramp_up_delay + 1); // Allow 1 second tolerance
952
953        // Test with None cycles - should return now + base ramp up delay
954        let ramp_up_start = mode.recommended_ramp_up_start(None);
955        assert!(ramp_up_start >= now + mode.base_ramp_up_delay);
956        assert!(ramp_up_start <= now + mode.base_ramp_up_delay + 1); // Allow 1 second tolerance
957
958        // Test with 1M cycles
959        let cycle_count = 1_000_000;
960        let executor_time = mode.executor_time(Some(cycle_count));
961        let expected_delay = mode.base_ramp_up_delay.max(executor_time as u64);
962        let ramp_up_start = mode.recommended_ramp_up_start(Some(cycle_count));
963        assert!(ramp_up_start >= now + expected_delay);
964        assert!(ramp_up_start <= now + expected_delay + 1); // Allow 1 second tolerance
965
966        // Test with 50M cycles
967        let cycle_count = 50_000_000;
968        let executor_time = mode.executor_time(Some(cycle_count));
969        let expected_delay = mode.base_ramp_up_delay.max(executor_time as u64);
970        let ramp_up_start = mode.recommended_ramp_up_start(Some(cycle_count));
971        assert!(ramp_up_start >= now + expected_delay);
972        assert!(ramp_up_start <= now + expected_delay + 1); // Allow 1 second tolerance
973    }
974}
975
976#[cfg(test)]
977mod tests {
978    use std::sync::Arc;
979
980    use alloy::{
981        network::TransactionBuilder,
982        node_bindings::Anvil,
983        primitives::Address,
984        providers::{DynProvider, Provider},
985        rpc::types::TransactionRequest,
986    };
987    use boundless_test_utils::{guests::ECHO_ELF, market::create_test_ctx};
988    use tracing_test::traced_test;
989    use url::Url;
990
991    use super::{
992        Adapt, Layer, OfferLayer, OfferLayerConfig, OfferParams, ParameterizationMode,
993        PreflightLayer, RequestBuilder, RequestId, RequestIdLayer, RequestIdLayerConfig,
994        RequestIdLayerMode, RequestParams, RequirementsLayer, StandardRequestBuilder, StorageLayer,
995        StorageLayerConfig,
996    };
997
998    use crate::prover_utils::{local_executor::LocalExecutor, prover::Prover};
999    use crate::storage::HttpDownloader;
1000    use crate::{
1001        contracts::{
1002            boundless_market::BoundlessMarketService, FulfillmentData, Predicate, RequestInput,
1003            RequestInputType, Requirements,
1004        },
1005        input::GuestEnv,
1006        storage::{MockStorageUploader, StandardDownloader, StorageDownloader, StorageUploader},
1007        util::NotProvided,
1008        StandardUploader,
1009    };
1010    use alloy_primitives::{utils::parse_ether, U256};
1011    use risc0_zkvm::{compute_image_id, sha::Digestible, Journal};
1012
1013    #[tokio::test]
1014    #[traced_test]
1015    async fn basic() -> anyhow::Result<()> {
1016        let anvil = Anvil::new().spawn();
1017        let test_ctx = create_test_ctx(&anvil).await.unwrap();
1018        let uploader = Arc::new(MockStorageUploader::new());
1019        let downloader = HttpDownloader::new(None, None);
1020        let market = BoundlessMarketService::new(
1021            test_ctx.deployment.boundless_market_address,
1022            test_ctx.customer_provider.clone(),
1023            test_ctx.customer_signer.address(),
1024        );
1025
1026        let request_builder = StandardRequestBuilder::builder()
1027            .storage_layer(Some(uploader))
1028            .preflight_layer(PreflightLayer::new(LocalExecutor::default(), Some(downloader)))
1029            .offer_layer(test_ctx.customer_provider.clone())
1030            .request_id_layer(market)
1031            .build()?;
1032
1033        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
1034        let request = request_builder.build(params).await?;
1035        println!("built request {request:#?}");
1036        Ok(())
1037    }
1038
1039    #[tokio::test]
1040    #[traced_test]
1041    async fn offer_layer_lock_collateral_default() -> anyhow::Result<()> {
1042        let anvil = Anvil::new().spawn();
1043        let test_ctx = create_test_ctx(&anvil).await.unwrap();
1044        let storage = Arc::new(MockStorageUploader::new());
1045        let downloader = HttpDownloader::new(None, None);
1046        let market = BoundlessMarketService::new(
1047            test_ctx.deployment.boundless_market_address,
1048            test_ctx.customer_provider.clone(),
1049            test_ctx.customer_signer.address(),
1050        );
1051
1052        let request_builder = StandardRequestBuilder::builder()
1053            .storage_layer(Some(storage))
1054            .preflight_layer(PreflightLayer::new(LocalExecutor::default(), Some(downloader)))
1055            .offer_layer(OfferLayer::new(
1056                test_ctx.customer_provider.clone(),
1057                OfferLayerConfig::builder().build()?,
1058            ))
1059            .request_id_layer(market)
1060            .build()?;
1061
1062        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
1063        let request = request_builder.build(params).await?;
1064        assert_eq!(request.offer.lockCollateral, parse_ether("5").unwrap());
1065        Ok(())
1066    }
1067
1068    #[tokio::test]
1069    #[traced_test]
1070    async fn with_offer_layer_settings() -> anyhow::Result<()> {
1071        let anvil = Anvil::new().spawn();
1072        let test_ctx = create_test_ctx(&anvil).await.unwrap();
1073        let uploader = Arc::new(MockStorageUploader::new());
1074        let downloader = HttpDownloader::new(None, None);
1075        let market = BoundlessMarketService::new(
1076            test_ctx.deployment.boundless_market_address,
1077            test_ctx.customer_provider.clone(),
1078            test_ctx.customer_signer.address(),
1079        );
1080
1081        let request_builder = StandardRequestBuilder::builder()
1082            .storage_layer(Some(uploader))
1083            .preflight_layer(PreflightLayer::new(LocalExecutor::default(), Some(downloader)))
1084            .offer_layer(OfferLayer::new(
1085                test_ctx.customer_provider.clone(),
1086                OfferLayerConfig::builder()
1087                    .ramp_up_period(27)
1088                    .lock_collateral(parse_ether("10").unwrap())
1089                    .build()?,
1090            ))
1091            .request_id_layer(market)
1092            .build()?;
1093
1094        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
1095        let request = request_builder.build(params).await?;
1096        assert_eq!(request.offer.rampUpPeriod, 27);
1097        assert_eq!(request.offer.lockCollateral, parse_ether("10").unwrap());
1098        Ok(())
1099    }
1100
1101    #[tokio::test]
1102    #[traced_test]
1103    async fn without_storage_uploader() -> anyhow::Result<()> {
1104        let anvil = Anvil::new().spawn();
1105        let test_ctx = create_test_ctx(&anvil).await.unwrap();
1106        let downloader = HttpDownloader::new(None, None);
1107        let market = BoundlessMarketService::new(
1108            test_ctx.deployment.boundless_market_address,
1109            test_ctx.customer_provider.clone(),
1110            test_ctx.customer_signer.address(),
1111        );
1112
1113        let request_builder = StandardRequestBuilder::builder::<_, NotProvided, _>()
1114            .preflight_layer(PreflightLayer::new(LocalExecutor::default(), Some(downloader)))
1115            .offer_layer(test_ctx.customer_provider.clone())
1116            .request_id_layer(market)
1117            .build()?;
1118
1119        // Try building the request by providing the program.
1120        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
1121        let err = request_builder.build(params).await.unwrap_err();
1122        tracing::debug!("err: {err}");
1123
1124        // Try again after uploading the program first.
1125        let uploader = Arc::new(MockStorageUploader::new());
1126        let program_url = uploader.upload_program(ECHO_ELF).await?;
1127        let params = request_builder.params().with_program_url(program_url)?.with_stdin(b"hello!");
1128        let request = request_builder.build(params).await?;
1129        let predicate = Predicate::try_from(request.requirements.predicate.clone())?;
1130        assert_eq!(predicate.image_id().unwrap(), risc0_zkvm::compute_image_id(ECHO_ELF)?);
1131        Ok(())
1132    }
1133
1134    #[tokio::test]
1135    #[traced_test]
1136    async fn test_storage_layer() -> anyhow::Result<()> {
1137        let uploader = Arc::new(MockStorageUploader::new());
1138        let downloader = HttpDownloader::new(None, None);
1139        let layer = StorageLayer::new(
1140            Some(uploader),
1141            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
1142        );
1143        let env = GuestEnv::from_stdin(b"inline_data");
1144        let (program_url, request_input) = layer.process((ECHO_ELF, &env)).await?;
1145
1146        // Program should be uploaded and input inline.
1147        assert_eq!(downloader.download_url(program_url).await?, ECHO_ELF);
1148        assert_eq!(request_input.inputType, RequestInputType::Inline);
1149        assert_eq!(request_input.data, env.encode()?);
1150        Ok(())
1151    }
1152
1153    #[tokio::test]
1154    #[traced_test]
1155    async fn test_storage_layer_no_provider() -> anyhow::Result<()> {
1156        let layer = StorageLayer::<NotProvided>::from(
1157            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
1158        );
1159
1160        let env = GuestEnv::from_stdin(b"inline_data");
1161        let request_input = layer.process(&env).await?;
1162
1163        // Program should be uploaded and input inline.
1164        assert_eq!(request_input.inputType, RequestInputType::Inline);
1165        assert_eq!(request_input.data, env.encode()?);
1166        Ok(())
1167    }
1168
1169    #[tokio::test]
1170    #[traced_test]
1171    async fn test_storage_layer_large_input() -> anyhow::Result<()> {
1172        let uploader = Arc::new(MockStorageUploader::new());
1173        let downloader = HttpDownloader::new(None, None);
1174        let layer = StorageLayer::new(
1175            Some(uploader),
1176            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
1177        );
1178        let env = GuestEnv::from_stdin(rand::random_iter().take(2048).collect::<Vec<u8>>());
1179        let (program_url, request_input) = layer.process((ECHO_ELF, &env)).await?;
1180
1181        // Program and input should be uploaded.
1182        assert_eq!(downloader.download_url(program_url).await?, ECHO_ELF);
1183        assert_eq!(request_input.inputType, RequestInputType::Url);
1184        let fetched_input =
1185            downloader.download(&String::from_utf8(request_input.data.to_vec())?).await?;
1186        assert_eq!(fetched_input, env.encode()?);
1187        Ok(())
1188    }
1189
1190    #[tokio::test]
1191    #[traced_test]
1192    async fn test_storage_layer_large_input_no_provider() -> anyhow::Result<()> {
1193        let layer = StorageLayer::from(
1194            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
1195        );
1196
1197        let env = GuestEnv::from_stdin(rand::random_iter().take(2048).collect::<Vec<u8>>());
1198        let err = layer.process(&env).await.unwrap_err();
1199
1200        assert!(err
1201            .to_string()
1202            .contains("cannot upload input using StorageLayer with no storage_uploader"));
1203        Ok(())
1204    }
1205
1206    #[tokio::test]
1207    #[traced_test]
1208    async fn test_preflight_layer() -> anyhow::Result<()> {
1209        let uploader = MockStorageUploader::new();
1210        let downloader = HttpDownloader::new(None, None);
1211        let program_url = uploader.upload_program(ECHO_ELF).await?;
1212        let layer = PreflightLayer::new(LocalExecutor::default(), Some(downloader));
1213        let data = b"hello_zkvm".to_vec();
1214        let env = GuestEnv::from_stdin(data.clone());
1215        let input = RequestInput::inline(env.encode()?);
1216
1217        // Create RequestParams and process through the layer
1218        let params = RequestParams::new().with_program_url(program_url)?.with_request_input(input);
1219        let result = params.process_with(&layer).await?;
1220
1221        assert_eq!(result.journal.unwrap().bytes, data);
1222        // Verify non-zero cycle count
1223        assert!(result.cycles.unwrap() > 0);
1224        Ok(())
1225    }
1226
1227    #[tokio::test]
1228    #[traced_test]
1229    async fn test_preflight_layer_cache_prefill() -> anyhow::Result<()> {
1230        // Create layer with shared executor
1231        let layer: PreflightLayer<HttpDownloader> = PreflightLayer::default();
1232        let executor = layer.executor_cloned();
1233
1234        // Create params with pre-computed values
1235        let image_id = risc0_zkvm::compute_image_id(ECHO_ELF)?;
1236        let input_data = b"test input";
1237        let cycles = 9999999u64;
1238        let journal = risc0_zkvm::Journal::new(input_data.to_vec());
1239
1240        // Build GuestEnv and encode as request input
1241        let env = GuestEnv { stdin: input_data.to_vec(), ..Default::default() };
1242        let request_input =
1243            RequestInput { inputType: RequestInputType::Inline, data: env.encode()?.into() };
1244
1245        let params = RequestParams::new()
1246            .with_image_id(image_id)
1247            .with_cycles(cycles)
1248            .with_journal(journal.clone())
1249            .with_request_input(request_input);
1250
1251        // Process through layer (should return early since cycles and journal are provided)
1252        let result = params.process_with(&layer).await?;
1253
1254        // Verify params unchanged
1255        assert_eq!(result.cycles, Some(cycles));
1256        assert_eq!(result.journal, Some(journal));
1257
1258        // Verify cache was pre-filled by checking we can get preflight result
1259        // First, upload image so preflight can find it
1260        executor.upload_image(&image_id.to_string(), ECHO_ELF.to_vec()).await?;
1261        let input_id = executor.upload_input(input_data.to_vec()).await?;
1262
1263        let preflight_result =
1264            executor.preflight(&image_id.to_string(), &input_id, vec![], None, "test").await?;
1265
1266        assert_eq!(preflight_result.stats.total_cycles, cycles);
1267
1268        Ok(())
1269    }
1270
1271    #[tokio::test]
1272    #[traced_test]
1273    async fn test_requirements_layer() -> anyhow::Result<()> {
1274        let layer = RequirementsLayer::default();
1275        let program = ECHO_ELF;
1276        let bytes = b"journal_data".to_vec();
1277        let journal = Journal::new(bytes.clone());
1278        let req = layer.process((program, &journal, &Default::default())).await?;
1279        let predicate = Predicate::try_from(req.predicate.clone())?;
1280        let fulfillment_data = FulfillmentData::from_image_id_and_journal(
1281            predicate.image_id().unwrap(),
1282            journal.bytes.clone(),
1283        );
1284        // Predicate should match the same journal
1285        assert!(predicate.eval(&fulfillment_data).is_some());
1286        // And should not match different data
1287        let other = Journal::new(b"other_data".to_vec());
1288        let fulfillment_data = FulfillmentData::from_image_id_and_journal(
1289            predicate.image_id().unwrap(),
1290            other.bytes.clone(),
1291        );
1292        assert!(predicate.eval(&fulfillment_data).is_none());
1293        Ok(())
1294    }
1295
1296    #[tokio::test]
1297    #[traced_test]
1298    async fn test_request_id_layer_rand() -> anyhow::Result<()> {
1299        let anvil = Anvil::new().spawn();
1300        let test_ctx = create_test_ctx(&anvil).await?;
1301        let market = BoundlessMarketService::new(
1302            test_ctx.deployment.boundless_market_address,
1303            test_ctx.customer_provider.clone(),
1304            test_ctx.customer_signer.address(),
1305        );
1306        let layer = RequestIdLayer::from(market.clone());
1307        assert_eq!(layer.config.mode, RequestIdLayerMode::Rand);
1308        let id = layer.process(()).await?;
1309        assert_eq!(id.addr, test_ctx.customer_signer.address());
1310        assert!(!id.smart_contract_signed);
1311        Ok(())
1312    }
1313
1314    #[tokio::test]
1315    #[traced_test]
1316    async fn test_request_id_layer_nonce() -> anyhow::Result<()> {
1317        let anvil = Anvil::new().spawn();
1318        let test_ctx = create_test_ctx(&anvil).await?;
1319        let market = BoundlessMarketService::new(
1320            test_ctx.deployment.boundless_market_address,
1321            test_ctx.customer_provider.clone(),
1322            test_ctx.customer_signer.address(),
1323        );
1324        let layer = RequestIdLayer::new(
1325            market.clone(),
1326            RequestIdLayerConfig::builder().mode(RequestIdLayerMode::Nonce).build()?,
1327        );
1328
1329        let id = layer.process(()).await?;
1330        assert_eq!(id.addr, test_ctx.customer_signer.address());
1331        // The customer address has sent no transactions.
1332        assert_eq!(id.index, 0);
1333        assert!(!id.smart_contract_signed);
1334
1335        // Send a tx then check that the index increments.
1336        let tx = TransactionRequest::default()
1337            .with_from(test_ctx.customer_signer.address())
1338            .with_to(Address::ZERO)
1339            .with_value(U256::from(1));
1340        test_ctx.customer_provider.send_transaction(tx).await?.watch().await?;
1341
1342        let id = layer.process(()).await?;
1343        assert_eq!(id.addr, test_ctx.customer_signer.address());
1344        // The customer address has sent one transaction.
1345        assert_eq!(id.index, 1);
1346        assert!(!id.smart_contract_signed);
1347
1348        Ok(())
1349    }
1350
1351    #[tokio::test]
1352    #[traced_test]
1353    async fn test_offer_layer_estimates() -> anyhow::Result<()> {
1354        // Use Anvil-backed provider for gas price
1355        let anvil = Anvil::new().spawn();
1356        let test_ctx = create_test_ctx(&anvil).await?;
1357        let provider = test_ctx.customer_provider.clone();
1358        let layer = OfferLayer::from(provider.clone());
1359        // Build minimal requirements and request ID
1360        let image_id = compute_image_id(ECHO_ELF).unwrap();
1361        let predicate = Predicate::digest_match(image_id, Journal::new(b"hello".to_vec()).digest());
1362        let requirements = Requirements::new(predicate);
1363        let request_id = RequestId::new(test_ctx.customer_signer.address(), 0);
1364
1365        // Zero cycles
1366        let offer_params = OfferParams::default();
1367        let now = crate::util::now_timestamp();
1368        let offer_zero_mcycles =
1369            layer.process((&requirements, &request_id, Some(0u64), &offer_params)).await?;
1370        assert_eq!(offer_zero_mcycles.minPrice, U256::ZERO);
1371        // Defaults from builder
1372        assert_eq!(
1373            offer_zero_mcycles.rampUpPeriod,
1374            ParameterizationMode::DEFAULT_BASE_RAMP_UP_PERIOD
1375        );
1376        assert_eq!(offer_zero_mcycles.lockTimeout, ParameterizationMode::DEFAULT_BASE_TIMEOUT);
1377        assert_eq!(offer_zero_mcycles.timeout, ParameterizationMode::DEFAULT_BASE_TIMEOUT * 2);
1378        // Default ramp up start should be now + base ramp up delay
1379        assert!(
1380            offer_zero_mcycles.rampUpStart
1381                >= now + ParameterizationMode::DEFAULT_BASE_RAMP_UP_DELAY
1382        );
1383        assert!(
1384            offer_zero_mcycles.rampUpStart
1385                <= now + ParameterizationMode::DEFAULT_BASE_RAMP_UP_DELAY + 1
1386        ); // Allow 1 second tolerance
1387           // Max price should be non-negative, to account for fixed costs.
1388        assert!(offer_zero_mcycles.maxPrice > U256::ZERO);
1389
1390        // Now create an offer for 100 Mcycles.
1391        let offer_more_mcycles =
1392            layer.process((&requirements, &request_id, Some(100u64 << 20), &offer_params)).await?;
1393        assert!(offer_more_mcycles.maxPrice > offer_zero_mcycles.maxPrice);
1394
1395        // Check that overrides are respected.
1396        let min_price = U256::from(1u64);
1397        let max_price = U256::from(5u64);
1398        let bidding_start = now + 100;
1399        let offer_params = OfferParams::builder()
1400            .max_price(max_price)
1401            .min_price(min_price)
1402            .bidding_start(bidding_start)
1403            .ramp_up_period(20)
1404            .lock_timeout(50)
1405            .timeout(80)
1406            .into();
1407        let offer_zero_mcycles =
1408            layer.process((&requirements, &request_id, Some(0u64), &offer_params)).await?;
1409        assert_eq!(offer_zero_mcycles.maxPrice, max_price);
1410        assert_eq!(offer_zero_mcycles.minPrice, min_price);
1411        assert_eq!(offer_zero_mcycles.rampUpPeriod, 20);
1412        assert_eq!(offer_zero_mcycles.lockTimeout, 50);
1413        assert_eq!(offer_zero_mcycles.timeout, 80);
1414        assert_eq!(offer_zero_mcycles.rampUpStart, bidding_start);
1415        Ok(())
1416    }
1417
1418    #[tokio::test]
1419    #[traced_test]
1420    async fn test_offer_layer_with_parameterization_mode() -> anyhow::Result<()> {
1421        // Use Anvil-backed provider for gas price
1422        let anvil = Anvil::new().spawn();
1423        let test_ctx = create_test_ctx(&anvil).await?;
1424        let provider = test_ctx.customer_provider.clone();
1425
1426        // Build minimal requirements and request ID
1427        let image_id = compute_image_id(ECHO_ELF).unwrap();
1428        let predicate = Predicate::digest_match(image_id, Journal::new(b"hello".to_vec()).digest());
1429        let requirements = Requirements::new(predicate);
1430        let request_id = RequestId::new(test_ctx.customer_signer.address(), 0);
1431
1432        // Test with fulfillment mode
1433        let fulfillment_mode = ParameterizationMode::fulfillment();
1434        let layer = OfferLayer::new(
1435            provider.clone(),
1436            OfferLayerConfig::builder().parameterization_mode(fulfillment_mode).build()?,
1437        );
1438        let now = crate::util::now_timestamp();
1439        let cycle_count = 100_000_000; // 100M cycles
1440        let offer_params = OfferParams::default();
1441        let offer =
1442            layer.process((&requirements, &request_id, Some(cycle_count), &offer_params)).await?;
1443
1444        // Check that ramp up start is calculated based on parameterization mode
1445        let expected_executor_time = fulfillment_mode.executor_time(Some(cycle_count));
1446        let expected_delay = fulfillment_mode.base_ramp_up_delay.max(expected_executor_time as u64);
1447        assert!(offer.rampUpStart >= now + expected_delay);
1448        assert!(offer.rampUpStart <= now + expected_delay + 1); // Allow 1 second tolerance
1449
1450        // Check that ramp up period is calculated based on parameterization mode
1451        let expected_ramp_up_period =
1452            fulfillment_mode.recommended_ramp_up_period(Some(cycle_count));
1453        assert_eq!(offer.rampUpPeriod, expected_ramp_up_period);
1454
1455        Ok(())
1456    }
1457
1458    #[test]
1459    fn request_params_with_program_url_infallible() {
1460        // When passing a parsed URL, with_program_url should be infallible.
1461        // NOTE: The `match *e {}` incantation is a compile-time assert that this error cannot
1462        // occur.
1463        let url = Url::parse("https://fileserver.example/guest.bin").unwrap();
1464        RequestParams::new().with_program_url(url).inspect_err(|e| match *e {}).unwrap();
1465    }
1466
1467    #[test]
1468    fn request_params_with_input_url_infallible() {
1469        // When passing a parsed URL, with_input_url should be infallible.
1470        // NOTE: The `match *e {}` incantation is a compile-time assert that this error cannot
1471        // occur.
1472        let url = Url::parse("https://fileserver.example/input.bin").unwrap();
1473        RequestParams::new().with_input_url(url).inspect_err(|e| match *e {}).unwrap();
1474    }
1475
1476    #[test]
1477    fn test_with_input_url() {
1478        // Test with string URL
1479        let params =
1480            RequestParams::new().with_input_url("https://fileserver.example/input.bin").unwrap();
1481
1482        let input = params.request_input.unwrap();
1483        assert_eq!(input.inputType, RequestInputType::Url);
1484        assert_eq!(input.data.as_ref(), "https://fileserver.example/input.bin".as_bytes());
1485
1486        // Test with parsed URL
1487        let url = Url::parse("https://fileserver.example/input2.bin").unwrap();
1488        let params = RequestParams::new().with_input_url(url).unwrap();
1489
1490        let input = params.request_input.unwrap();
1491        assert_eq!(input.inputType, RequestInputType::Url);
1492        assert_eq!(input.data.as_ref(), "https://fileserver.example/input2.bin".as_bytes());
1493    }
1494
1495    #[allow(dead_code)]
1496    trait AssertSend: Send {}
1497
1498    // The StandardRequestBuilder must be Send such that a Client can be sent between threads.
1499    impl AssertSend for StandardRequestBuilder<DynProvider, StandardUploader, StandardDownloader> {}
1500}