boundless_market/request_builder/
mod.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::{
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_ethereum_contracts::selector::Selector;
28use risc0_zkvm::{Digest, Journal};
29use url::Url;
30
31use crate::{
32    contracts::{ProofRequest, RequestId, RequestInput},
33    input::GuestEnv,
34    storage::{StandardStorageProvider, StorageProvider},
35    util::NotProvided,
36};
37mod preflight_layer;
38mod storage_layer;
39
40pub use preflight_layer::PreflightLayer;
41pub use storage_layer::{StorageLayer, StorageLayerConfig, StorageLayerConfigBuilder};
42mod requirements_layer;
43pub use requirements_layer::{RequirementParams, RequirementsLayer};
44mod request_id_layer;
45pub use request_id_layer::{
46    RequestIdLayer, RequestIdLayerConfig, RequestIdLayerConfigBuilder, RequestIdLayerMode,
47};
48mod offer_layer;
49pub use offer_layer::{
50    OfferLayer, OfferLayerConfig, OfferLayerConfigBuilder, OfferParams, OfferParamsBuilder,
51};
52mod finalizer;
53pub use finalizer::{Finalizer, FinalizerConfig, FinalizerConfigBuilder};
54
55/// A trait for building proof requests, used by the [Client][crate::Client].
56///
57/// See [StandardRequestBuilder] for an example implementation.
58pub trait RequestBuilder<Params> {
59    /// Error type that may be returned by this builder.
60    type Error;
61
62    // NOTE: Takes the self receiver so that the caller does not need to explicitly name the
63    // RequestBuilder trait (e.g. `<MyRequestBuilder as RequestBuilder>::params()`). This could
64    // also be used to set initial values on the params that are specific to the rrequest builder.
65    /// Returns a default instance of the parameter type used by this builder.
66    fn params(&self) -> Params
67    where
68        Params: Default,
69    {
70        Default::default()
71    }
72
73    /// Builds a [ProofRequest] using the provided parameters.
74    fn build(
75        &self,
76        params: impl Into<Params>,
77    ) -> impl Future<Output = Result<ProofRequest, Self::Error>>;
78}
79
80/// Blanket implementation for [RequestBuilder] for all [Layer] that output a proof request.
81///
82/// This implementation allows for custom and modified layered builders to automatically be usable
83/// as a [RequestBuilder].
84impl<L, Params> RequestBuilder<Params> for L
85where
86    L: Layer<Params, Output = ProofRequest>,
87{
88    type Error = L::Error;
89
90    async fn build(&self, params: impl Into<Params>) -> Result<ProofRequest, Self::Error> {
91        self.process(params.into()).await
92    }
93}
94
95/// A trait representing a processing layer in a request building pipeline.
96///
97/// Layers can be composed together to form a multi-step processing pipeline where the output
98/// of one layer becomes the input to the next. Each layer handles a specific aspect of the
99/// request building process.
100pub trait Layer<Input> {
101    /// The output type produced by this layer.
102    type Output;
103
104    /// Error type that may be returned by this layer.
105    type Error;
106
107    /// Processes the input and returns the transformed output.
108    fn process(&self, input: Input) -> impl Future<Output = Result<Self::Output, Self::Error>>;
109}
110
111/// A trait for adapting types to be processed by a [Layer].
112///
113/// This trait provides a mechanism for a type to be processed by a layer, enabling
114/// the composition of multiple layers into a processing pipeline. Inputs are adapted
115/// to work with specific layer types, with the output of one layer feeding into the next.
116///
117/// Existing [Layer] implementations can be adapted to work with new parameter types by
118/// implementating `Adapt<Layer>` on the parameter type.
119pub trait Adapt<L> {
120    /// The output type after processing by the layer.
121    type Output;
122
123    /// Error type that may be returned during processing.
124    type Error;
125
126    /// Processes this value with the provided layer.
127    fn process_with(self, layer: &L) -> impl Future<Output = Result<Self::Output, Self::Error>>;
128}
129
130impl<L, I> Adapt<L> for I
131where
132    L: Layer<I>,
133{
134    type Output = L::Output;
135    type Error = L::Error;
136
137    async fn process_with(self, layer: &L) -> Result<Self::Output, Self::Error> {
138        layer.process(self).await
139    }
140}
141
142/// Define a layer as a stack of two layers. Output of layer A is piped into layer B.
143impl<A, B, Input> Layer<Input> for (A, B)
144where
145    Input: Adapt<A>,
146    <Input as Adapt<A>>::Output: Adapt<B>,
147    <Input as Adapt<A>>::Error: Into<<<Input as Adapt<A>>::Output as Adapt<B>>::Error>,
148{
149    type Output = <<Input as Adapt<A>>::Output as Adapt<B>>::Output;
150    type Error = <<Input as Adapt<A>>::Output as Adapt<B>>::Error;
151
152    async fn process(&self, input: Input) -> Result<Self::Output, Self::Error> {
153        input.process_with(&self.0).await.map_err(Into::into)?.process_with(&self.1).await
154    }
155}
156
157/// A standard implementation of [RequestBuilder] that uses a layered architecture.
158///
159/// This builder composes multiple layers, each handling a specific aspect of request building:
160/// - `storage_layer`: Manages program and input storage
161/// - `preflight_layer`: Validates and simulates the request
162/// - `requirements_layer`: Sets up verification requirements
163/// - `request_id_layer`: Manages request identifier generation
164/// - `offer_layer`: Configures the offer details
165/// - `finalizer`: Validates and finalizes the request
166///
167/// Each layer processes the request in sequence, with the output of one layer becoming
168/// the input for the next.
169#[derive(Clone, Builder)]
170#[non_exhaustive]
171pub struct StandardRequestBuilder<P = DynProvider, S = StandardStorageProvider> {
172    /// Handles uploading and preparing program and input data.
173    #[builder(setter(into))]
174    pub storage_layer: StorageLayer<S>,
175
176    /// Executes preflight checks to validate the request.
177    #[builder(setter(into), default)]
178    pub preflight_layer: PreflightLayer,
179
180    /// Configures the requirements for the proof request.
181    #[builder(setter(into), default)]
182    pub requirements_layer: RequirementsLayer,
183
184    /// Generates and manages request identifiers.
185    #[builder(setter(into))]
186    pub request_id_layer: RequestIdLayer<P>,
187
188    /// Configures offer parameters for the request.
189    #[builder(setter(into))]
190    pub offer_layer: OfferLayer<P>,
191
192    /// Finalizes and validates the complete request.
193    #[builder(setter(into), default)]
194    pub finalizer: Finalizer,
195}
196
197impl StandardRequestBuilder<NotProvided, NotProvided> {
198    /// Creates a new builder for constructing a [StandardRequestBuilder].
199    ///
200    /// This is the entry point for creating a request builder with specific
201    /// provider and storage implementations.
202    ///
203    /// # Type Parameters
204    /// * `P` - An Ethereum RPC provider, using alloy.
205    /// * `S` - The storage provider type for storing programs and inputs.
206    pub fn builder<P: Clone, S: Clone>() -> StandardRequestBuilderBuilder<P, S> {
207        Default::default()
208    }
209}
210
211impl<P, S> Layer<RequestParams> for StandardRequestBuilder<P, S>
212where
213    S: StorageProvider,
214    S::Error: std::error::Error + Send + Sync + 'static,
215    P: Provider<Ethereum> + 'static + Clone,
216{
217    type Output = ProofRequest;
218    type Error = anyhow::Error;
219
220    async fn process(&self, input: RequestParams) -> Result<ProofRequest, Self::Error> {
221        input
222            .process_with(&self.storage_layer)
223            .await?
224            .process_with(&self.preflight_layer)
225            .await?
226            .process_with(&self.requirements_layer)
227            .await?
228            .process_with(&self.request_id_layer)
229            .await?
230            .process_with(&self.offer_layer)
231            .await?
232            .process_with(&self.finalizer)
233            .await
234    }
235}
236
237impl<P> Layer<RequestParams> for StandardRequestBuilder<P, NotProvided>
238where
239    P: Provider<Ethereum> + 'static + Clone,
240{
241    type Output = ProofRequest;
242    type Error = anyhow::Error;
243
244    async fn process(&self, input: RequestParams) -> Result<ProofRequest, Self::Error> {
245        input
246            .process_with(&self.storage_layer)
247            .await?
248            .process_with(&self.preflight_layer)
249            .await?
250            .process_with(&self.requirements_layer)
251            .await?
252            .process_with(&self.request_id_layer)
253            .await?
254            .process_with(&self.offer_layer)
255            .await?
256            .process_with(&self.finalizer)
257            .await
258    }
259}
260
261// NOTE: We don't use derive_builder here because we need to be able to access the values on the
262// incrementally built parameters.
263/// Parameters for building a proof request.
264///
265/// This struct holds all the necessary information for constructing a [ProofRequest].
266/// It provides a builder pattern for incrementally setting fields and methods for
267/// validating and accessing the parameters.
268///
269/// Most fields are optional and can be populated during the request building process
270/// by various layers. The structure serves as the central data container that passes
271/// through the request builder pipeline.
272#[non_exhaustive]
273#[derive(Clone, Default)]
274pub struct RequestParams {
275    /// RISC-V guest program that will be run in the zkVM.
276    pub program: Option<Cow<'static, [u8]>>,
277
278    /// Guest execution environment, providing the input for the guest.
279    /// See [GuestEnv].
280    pub env: Option<GuestEnv>,
281
282    /// Uploaded program URL, from which provers will fetch the program.
283    pub program_url: Option<Url>,
284
285    /// Prepared input for the [ProofRequest], containing either a URL or inline input.
286    /// See [RequestInput].
287    pub request_input: Option<RequestInput>,
288
289    /// Count of the RISC Zero execution cycles. Used to estimate proving cost.
290    pub cycles: Option<u64>,
291
292    /// Image ID identifying the program being executed.
293    pub image_id: Option<Digest>,
294
295    /// Contents of the [Journal] that results from the execution.
296    pub journal: Option<Journal>,
297
298    /// [RequestId] to use for the proof request.
299    pub request_id: Option<RequestId>,
300
301    /// [OfferParams] for constructing the [Offer][crate::Offer] to send along with the request.
302    pub offer: OfferParams,
303
304    /// [RequirementParams] for constructing the [Requirements][crate::Requirements] for the resulting proof.
305    pub requirements: RequirementParams,
306}
307
308impl RequestParams {
309    /// Creates a new empty instance of [RequestParams].
310    ///
311    /// This is equivalent to calling `Default::default()` and is provided as a
312    /// convenience method for better readability when building requests.
313    pub fn new() -> Self {
314        Self::default()
315    }
316
317    /// Gets the program bytes, returning an error if not set.
318    ///
319    /// This method is used by layers in the request building pipeline to access
320    /// the program when it's required for processing.
321    pub fn require_program(&self) -> Result<&[u8], MissingFieldError> {
322        self.program
323            .as_deref()
324            .ok_or(MissingFieldError::with_hint("program", "can be set using .with_program(...)"))
325    }
326
327    /// Sets the program to be executed in the zkVM.
328    pub fn with_program(self, value: impl Into<Cow<'static, [u8]>>) -> Self {
329        Self { program: Some(value.into()), ..self }
330    }
331
332    /// Gets the guest environment, returning an error if not set.
333    ///
334    /// The guest environment contains the input data for the program.
335    pub fn require_env(&self) -> Result<&GuestEnv, MissingFieldError> {
336        self.env.as_ref().ok_or(MissingFieldError::with_hint(
337            "env",
338            "can be set using .with_env(...) or .with_stdin",
339        ))
340    }
341
342    /// Sets the [GuestEnv], providing the guest with input.
343    ///
344    /// Can be constructed with [GuestEnvBuilder][crate::input::GuestEnvBuilder].
345    ///
346    /// ```rust
347    /// # use boundless_market::request_builder::RequestParams;
348    /// # const ECHO_ELF: &[u8] = b"";
349    /// use boundless_market::GuestEnv;
350    ///
351    /// RequestParams::new()
352    ///     .with_program(ECHO_ELF)
353    ///     .with_env(GuestEnv::builder()
354    ///         .write_frame(b"hello!")
355    ///         .write_frame(b"goodbye."));
356    /// ```
357    ///
358    /// See also [Self::with_env] and [GuestEnvBuilder][crate::input::GuestEnvBuilder]
359    pub fn with_env(self, value: impl Into<GuestEnv>) -> Self {
360        Self { env: Some(value.into()), ..self }
361    }
362
363    /// Sets the [GuestEnv] to be contain the given bytes as `stdin`.
364    ///
365    /// Note that the bytes are passed directly to the guest without encoding. If your guest
366    /// expects the input to be encoded in any way (e.g. `bincode`), the caller must encode the
367    /// data before passing it.
368    ///
369    /// If the [GuestEnv] is already set, this replaces it.
370    ///
371    /// ```rust
372    /// # use boundless_market::request_builder::RequestParams;
373    /// # const ECHO_ELF: &[u8] = b"";
374    /// RequestParams::new()
375    ///     .with_program(ECHO_ELF)
376    ///     .with_stdin(b"hello!");
377    /// ```
378    ///
379    /// See also [Self::with_env] and [GuestEnvBuilder][crate::input::GuestEnvBuilder]
380    pub fn with_stdin(self, value: impl Into<Vec<u8>>) -> Self {
381        Self { env: Some(GuestEnv::from_stdin(value)), ..self }
382    }
383
384    /// Gets the program URL, returning an error if not set.
385    ///
386    /// The program URL is where provers will download the program to execute.
387    pub fn require_program_url(&self) -> Result<&Url, MissingFieldError> {
388        self.program_url.as_ref().ok_or(MissingFieldError::with_hint(
389            "program_url",
390            "can be set using .with_program_url(...)",
391        ))
392    }
393
394    /// Set the program URL, where provers can download the program to be proven.
395    ///
396    /// ```rust
397    /// # use boundless_market::request_builder::RequestParams;
398    /// # || -> anyhow::Result<()> {
399    /// RequestParams::new()
400    ///     .with_program_url("https://fileserver.example/guest.bin")?;
401    /// # Ok(())
402    /// # }().unwrap();
403    /// ```
404    pub fn with_program_url<T: TryInto<Url>>(self, value: T) -> Result<Self, T::Error> {
405        Ok(Self { program_url: Some(value.try_into()?), ..self })
406    }
407
408    /// Gets the request input, returning an error if not set.
409    ///
410    /// The request input contains the input data for the guest program, either inline or as a URL.
411    pub fn require_request_input(&self) -> Result<&RequestInput, MissingFieldError> {
412        self.request_input.as_ref().ok_or(MissingFieldError::with_hint(
413            "request_input",
414            "can be set using .with_request_input(...)",
415        ))
416    }
417
418    /// Sets the encoded input data for the request. This data will be decoded by the prover into a
419    /// [GuestEnv] that will be used to run the guest.
420    ///
421    /// If not provided, the this will be constructed from the data given via
422    /// [RequestParams::with_env] or [RequestParams::with_stdin]. If the input is set with both
423    /// this method and one of those two, the input specified here takes precedence.
424    pub fn with_request_input(self, value: impl Into<RequestInput>) -> Self {
425        Self { request_input: Some(value.into()), ..self }
426    }
427
428    /// Sets the input as a URL from which provers can download the input data.
429    ///
430    /// This is a convenience method that creates a [RequestInput] with URL type.
431    ///
432    /// ```rust
433    /// # use boundless_market::request_builder::RequestParams;
434    /// # || -> anyhow::Result<()> {
435    /// RequestParams::new()
436    ///     .with_input_url("https://fileserver.example/input.bin")?;
437    /// # Ok(())
438    /// # }().unwrap();
439    /// ```
440    pub fn with_input_url<T: TryInto<Url>>(self, value: T) -> Result<Self, T::Error> {
441        Ok(Self { request_input: Some(RequestInput::url(value.try_into()?)), ..self })
442    }
443
444    /// Gets the cycle count, returning an error if not set.
445    ///
446    /// The cycle count is used to estimate proving costs.
447    pub fn require_cycles(&self) -> Result<u64, MissingFieldError> {
448        self.cycles
449            .ok_or(MissingFieldError::with_hint("cycles", "can be set using .with_cycles(...)"))
450    }
451
452    /// Sets the cycle count for the proof request.
453    ///
454    /// This is used to estimate proving costs and determine appropriate pricing.
455    pub fn with_cycles(self, value: u64) -> Self {
456        Self { cycles: Some(value), ..self }
457    }
458
459    /// Gets the journal, returning an error if not set.
460    ///
461    /// The journal contains the output from the guest program execution.
462    pub fn require_journal(&self) -> Result<&Journal, MissingFieldError> {
463        self.journal
464            .as_ref()
465            .ok_or(MissingFieldError::with_hint("journal", "can be set using .with_journal(...)"))
466    }
467
468    /// Sets the journal for the request.
469    ///
470    /// The journal is the output from the guest program execution and is used
471    /// to configure verification requirements.
472    pub fn with_journal(self, value: impl Into<Journal>) -> Self {
473        Self { journal: Some(value.into()), ..self }
474    }
475
476    /// Gets the image ID, returning an error if not set.
477    ///
478    /// The image ID uniquely identifies the program being executed.
479    pub fn require_image_id(&self) -> Result<Digest, MissingFieldError> {
480        self.image_id.ok_or(MissingFieldError::with_hint(
481            "image_id",
482            "can be set using .with_image_id(...), and is calculated from the program",
483        ))
484    }
485
486    /// Sets the image ID for the request.
487    ///
488    /// The image ID is the hash of the program binary and uniquely identifies
489    /// the program being executed.
490    pub fn with_image_id(self, value: impl Into<Digest>) -> Self {
491        Self { image_id: Some(value.into()), ..self }
492    }
493
494    /// Gets the request ID, returning an error if not set.
495    ///
496    /// The request ID contains the requestor's address and a unique index,
497    /// and is used to track the request throughout its lifecycle.
498    pub fn require_request_id(&self) -> Result<&RequestId, MissingFieldError> {
499        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"))
500    }
501
502    /// Sets the request ID for the proof request.
503    ///
504    /// The request ID contains the requestor's address and a unique index,
505    /// and is used to track the request throughout its lifecycle.
506    pub fn with_request_id(self, value: impl Into<RequestId>) -> Self {
507        Self { request_id: Some(value.into()), ..self }
508    }
509
510    /// Configure the [Offer][crate::Offer] on the [ProofRequest] by either providing a complete
511    /// offer, or a partial offer via [OfferParams].
512    ///
513    /// ```rust
514    /// # use boundless_market::request_builder::{RequestParams, OfferParams};
515    /// use alloy::primitives::utils::parse_units;
516    ///
517    /// RequestParams::new()
518    ///     .with_offer(OfferParams::builder()
519    ///         .max_price(parse_units("0.01", "ether").unwrap())
520    ///         .ramp_up_period(30)
521    ///         .lock_timeout(120)
522    ///         .timeout(240));
523    /// ```
524    pub fn with_offer(self, value: impl Into<OfferParams>) -> Self {
525        Self { offer: value.into(), ..self }
526    }
527
528    /// Configure the [Requirements][crate::Requirements] on the [ProofRequest] by either providing
529    /// the complete requirements, or partial requirements via [RequirementParams].
530    ///
531    /// ```rust
532    /// # use boundless_market::request_builder::{RequestParams, RequirementParams};
533    /// use alloy::primitives::address;
534    ///
535    /// RequestParams::new()
536    ///     .with_requirements(RequirementParams::builder()
537    ///         .callback_address(address!("0x00000000000000000000000000000000deadbeef")));
538    /// ```
539    pub fn with_requirements(self, value: impl Into<RequirementParams>) -> Self {
540        Self { requirements: value.into(), ..self }
541    }
542
543    /// Request a stand-alone Groth16 proof for this request.
544    ///
545    /// This is a convinience method to set the selector on the requirements. Note that calling
546    /// [RequestParams::with_requirements] after this function will overwrite the change.
547    pub fn with_groth16_proof(self) -> Self {
548        let mut requirements = self.requirements;
549        requirements.selector = match crate::util::is_dev_mode() {
550            true => Some((Selector::FakeReceipt as u32).into()),
551            false => Some((Selector::groth16_latest() as u32).into()),
552        };
553        Self { requirements, ..self }
554    }
555}
556
557impl Debug for RequestParams {
558    /// [Debug] implementation that does not print the contents of the program.
559    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560        f.debug_struct("ExampleRequestParams")
561            .field("program", &self.program.as_ref().map(|x| format!("[{} bytes]", x.len())))
562            .field("env", &self.env)
563            .field("program_url", &self.program_url)
564            .field("input", &self.request_input)
565            .field("cycles", &self.cycles)
566            .field("journal", &self.journal)
567            .field("request_id", &self.request_id)
568            .field("offer", &self.offer)
569            .field("requirements", &self.requirements)
570            .finish()
571    }
572}
573
574impl<Program, Env> From<(Program, Env)> for RequestParams
575where
576    Program: Into<Cow<'static, [u8]>>,
577    Env: Into<GuestEnv>,
578{
579    fn from(value: (Program, Env)) -> Self {
580        Self::default().with_program(value.0).with_env(value.1)
581    }
582}
583
584/// Error indicating that a required field is missing when building a request.
585///
586/// This error is returned when attempting to access a field that hasn't been
587/// set yet in the request parameters.
588#[derive(Debug)]
589pub struct MissingFieldError {
590    /// The name of the missing field.
591    pub label: Cow<'static, str>,
592    /// An optional hint as to the cause of the error, or how to resolve it.
593    pub hint: Option<Cow<'static, str>>,
594}
595
596impl Display for MissingFieldError {
597    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598        match self.hint {
599            None => write!(f, "field `{}` is required but is uninitialized", self.label),
600            Some(ref hint) => {
601                write!(f, "field `{}` is required but is uninitialized; {hint}", self.label)
602            }
603        }
604    }
605}
606
607impl std::error::Error for MissingFieldError {}
608
609impl MissingFieldError {
610    /// Creates a new error for the specified missing field.
611    pub fn new(label: impl Into<Cow<'static, str>>) -> Self {
612        Self { label: label.into(), hint: None }
613    }
614
615    /// Creates a new error for the specified missing field.
616    pub fn with_hint(
617        label: impl Into<Cow<'static, str>>,
618        hint: impl Into<Cow<'static, str>>,
619    ) -> Self {
620        Self { label: label.into(), hint: Some(hint.into()) }
621    }
622}
623
624#[cfg(test)]
625mod tests {
626    use std::sync::Arc;
627
628    use alloy::{
629        network::TransactionBuilder,
630        node_bindings::Anvil,
631        primitives::Address,
632        providers::{DynProvider, Provider},
633        rpc::types::TransactionRequest,
634    };
635    use boundless_market_test_utils::{create_test_ctx, ECHO_ELF};
636    use tracing_test::traced_test;
637    use url::Url;
638
639    use super::{
640        Layer, OfferLayer, OfferLayerConfig, OfferParams, PreflightLayer, RequestBuilder,
641        RequestId, RequestIdLayer, RequestIdLayerConfig, RequestIdLayerMode, RequestParams,
642        RequirementsLayer, StandardRequestBuilder, StorageLayer, StorageLayerConfig,
643    };
644
645    use crate::{
646        contracts::{
647            boundless_market::BoundlessMarketService, Predicate, RequestInput, RequestInputType,
648            Requirements,
649        },
650        input::GuestEnv,
651        storage::{fetch_url, MockStorageProvider, StorageProvider},
652        util::NotProvided,
653        StandardStorageProvider,
654    };
655    use alloy_primitives::U256;
656    use risc0_zkvm::{compute_image_id, sha::Digestible, Journal};
657
658    #[tokio::test]
659    #[traced_test]
660    async fn basic() -> anyhow::Result<()> {
661        let anvil = Anvil::new().spawn();
662        let test_ctx = create_test_ctx(&anvil).await.unwrap();
663        let storage = Arc::new(MockStorageProvider::start());
664        let market = BoundlessMarketService::new(
665            test_ctx.deployment.boundless_market_address,
666            test_ctx.customer_provider.clone(),
667            test_ctx.customer_signer.address(),
668        );
669
670        let request_builder = StandardRequestBuilder::builder()
671            .storage_layer(Some(storage))
672            .offer_layer(test_ctx.customer_provider.clone())
673            .request_id_layer(market)
674            .build()?;
675
676        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
677        let request = request_builder.build(params).await?;
678        println!("built request {request:#?}");
679        Ok(())
680    }
681
682    #[tokio::test]
683    #[traced_test]
684    async fn with_offer_layer_settings() -> anyhow::Result<()> {
685        let anvil = Anvil::new().spawn();
686        let test_ctx = create_test_ctx(&anvil).await.unwrap();
687        let storage = Arc::new(MockStorageProvider::start());
688        let market = BoundlessMarketService::new(
689            test_ctx.deployment.boundless_market_address,
690            test_ctx.customer_provider.clone(),
691            test_ctx.customer_signer.address(),
692        );
693
694        let request_builder = StandardRequestBuilder::builder()
695            .storage_layer(Some(storage))
696            .offer_layer(OfferLayer::new(
697                test_ctx.customer_provider.clone(),
698                OfferLayerConfig::builder().ramp_up_period(27).build()?,
699            ))
700            .request_id_layer(market)
701            .build()?;
702
703        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
704        let request = request_builder.build(params).await?;
705        assert_eq!(request.offer.rampUpPeriod, 27);
706        Ok(())
707    }
708
709    #[tokio::test]
710    #[traced_test]
711    async fn without_storage_provider() -> anyhow::Result<()> {
712        let anvil = Anvil::new().spawn();
713        let test_ctx = create_test_ctx(&anvil).await.unwrap();
714        let market = BoundlessMarketService::new(
715            test_ctx.deployment.boundless_market_address,
716            test_ctx.customer_provider.clone(),
717            test_ctx.customer_signer.address(),
718        );
719
720        let request_builder = StandardRequestBuilder::builder()
721            .storage_layer(None::<NotProvided>)
722            .offer_layer(test_ctx.customer_provider.clone())
723            .request_id_layer(market)
724            .build()?;
725
726        // Try building the reqeust by providing the program.
727        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
728        let err = request_builder.build(params).await.unwrap_err();
729        tracing::debug!("err: {err}");
730
731        // Try again after uploading the program first.
732        let storage = Arc::new(MockStorageProvider::start());
733        let program_url = storage.upload_program(ECHO_ELF).await?;
734        let params = request_builder.params().with_program_url(program_url)?.with_stdin(b"hello!");
735        let request = request_builder.build(params).await?;
736        assert_eq!(
737            request.requirements.imageId,
738            risc0_zkvm::compute_image_id(ECHO_ELF)?.as_bytes()
739        );
740        Ok(())
741    }
742
743    #[tokio::test]
744    #[traced_test]
745    async fn test_storage_layer() -> anyhow::Result<()> {
746        let storage = Arc::new(MockStorageProvider::start());
747        let layer = StorageLayer::new(
748            Some(storage.clone()),
749            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
750        );
751        let env = GuestEnv::from_stdin(b"inline_data");
752        let (program_url, request_input) = layer.process((ECHO_ELF, &env)).await?;
753
754        // Program should be uploaded and input inline.
755        assert_eq!(fetch_url(&program_url).await?, ECHO_ELF);
756        assert_eq!(request_input.inputType, RequestInputType::Inline);
757        assert_eq!(request_input.data, env.encode()?);
758        Ok(())
759    }
760
761    #[tokio::test]
762    #[traced_test]
763    async fn test_storage_layer_no_provider() -> anyhow::Result<()> {
764        let layer = StorageLayer::<NotProvided>::from(
765            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
766        );
767
768        let env = GuestEnv::from_stdin(b"inline_data");
769        let request_input = layer.process(&env).await?;
770
771        // Program should be uploaded and input inline.
772        assert_eq!(request_input.inputType, RequestInputType::Inline);
773        assert_eq!(request_input.data, env.encode()?);
774        Ok(())
775    }
776
777    #[tokio::test]
778    #[traced_test]
779    async fn test_storage_layer_large_input() -> anyhow::Result<()> {
780        let storage = Arc::new(MockStorageProvider::start());
781        let layer = StorageLayer::new(
782            Some(storage.clone()),
783            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
784        );
785        let env = GuestEnv::from_stdin(rand::random_iter().take(2048).collect::<Vec<u8>>());
786        let (program_url, request_input) = layer.process((ECHO_ELF, &env)).await?;
787
788        // Program and input should be uploaded and input inline.
789        assert_eq!(fetch_url(&program_url).await?, ECHO_ELF);
790        assert_eq!(request_input.inputType, RequestInputType::Url);
791        let fetched_input = fetch_url(String::from_utf8(request_input.data.to_vec())?).await?;
792        assert_eq!(fetched_input, env.encode()?);
793        Ok(())
794    }
795
796    #[tokio::test]
797    #[traced_test]
798    async fn test_storage_layer_large_input_no_provider() -> anyhow::Result<()> {
799        let layer = StorageLayer::from(
800            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
801        );
802
803        let env = GuestEnv::from_stdin(rand::random_iter().take(2048).collect::<Vec<u8>>());
804        let err = layer.process(&env).await.unwrap_err();
805
806        assert!(err
807            .to_string()
808            .contains("cannot upload input using StorageLayer with no storage_provider"));
809        Ok(())
810    }
811
812    #[tokio::test]
813    #[traced_test]
814    async fn test_preflight_layer() -> anyhow::Result<()> {
815        let storage = MockStorageProvider::start();
816        let program_url = storage.upload_program(ECHO_ELF).await?;
817        let layer = PreflightLayer::default();
818        let data = b"hello_zkvm".to_vec();
819        let env = GuestEnv::from_stdin(data.clone());
820        let input = RequestInput::inline(env.encode()?);
821        let session = layer.process((&program_url, &input)).await?;
822
823        assert_eq!(session.journal.as_ref(), data.as_slice());
824        // Verify non-zero cycle count and an exit code of zero.
825        let cycles: u64 = session.segments.iter().map(|s| 1 << s.po2).sum();
826        assert!(cycles > 0);
827        assert!(session.exit_code.is_ok());
828        Ok(())
829    }
830
831    #[tokio::test]
832    #[traced_test]
833    async fn test_requirements_layer() -> anyhow::Result<()> {
834        let layer = RequirementsLayer::default();
835        let program = ECHO_ELF;
836        let bytes = b"journal_data".to_vec();
837        let journal = Journal::new(bytes.clone());
838        let req = layer.process((program, &journal, &Default::default())).await?;
839
840        // Predicate should match the same journal
841        assert!(req.predicate.eval(&journal));
842        // And should not match different data
843        let other = Journal::new(b"other_data".to_vec());
844        assert!(!req.predicate.eval(&other));
845        Ok(())
846    }
847
848    #[tokio::test]
849    #[traced_test]
850    async fn test_request_id_layer_rand() -> anyhow::Result<()> {
851        let anvil = Anvil::new().spawn();
852        let test_ctx = create_test_ctx(&anvil).await?;
853        let market = BoundlessMarketService::new(
854            test_ctx.deployment.boundless_market_address,
855            test_ctx.customer_provider.clone(),
856            test_ctx.customer_signer.address(),
857        );
858        let layer = RequestIdLayer::from(market.clone());
859        assert_eq!(layer.config.mode, RequestIdLayerMode::Rand);
860        let id = layer.process(()).await?;
861        assert_eq!(id.addr, test_ctx.customer_signer.address());
862        assert!(!id.smart_contract_signed);
863        Ok(())
864    }
865
866    #[tokio::test]
867    #[traced_test]
868    async fn test_request_id_layer_nonce() -> anyhow::Result<()> {
869        let anvil = Anvil::new().spawn();
870        let test_ctx = create_test_ctx(&anvil).await?;
871        let market = BoundlessMarketService::new(
872            test_ctx.deployment.boundless_market_address,
873            test_ctx.customer_provider.clone(),
874            test_ctx.customer_signer.address(),
875        );
876        let layer = RequestIdLayer::new(
877            market.clone(),
878            RequestIdLayerConfig::builder().mode(RequestIdLayerMode::Nonce).build()?,
879        );
880
881        let id = layer.process(()).await?;
882        assert_eq!(id.addr, test_ctx.customer_signer.address());
883        // The customer address has sent no transactions.
884        assert_eq!(id.index, 0);
885        assert!(!id.smart_contract_signed);
886
887        // Send a tx then check that the index increments.
888        let tx = TransactionRequest::default()
889            .with_from(test_ctx.customer_signer.address())
890            .with_to(Address::ZERO)
891            .with_value(U256::from(1));
892        test_ctx.customer_provider.send_transaction(tx).await?.watch().await?;
893
894        let id = layer.process(()).await?;
895        assert_eq!(id.addr, test_ctx.customer_signer.address());
896        // The customer address has sent one transaction.
897        assert_eq!(id.index, 1);
898        assert!(!id.smart_contract_signed);
899
900        Ok(())
901    }
902
903    #[tokio::test]
904    #[traced_test]
905    async fn test_offer_layer_estimates() -> anyhow::Result<()> {
906        // Use Anvil-backed provider for gas price
907        let anvil = Anvil::new().spawn();
908        let test_ctx = create_test_ctx(&anvil).await?;
909        let provider = test_ctx.customer_provider.clone();
910        let layer = OfferLayer::from(provider.clone());
911        // Build minimal requirements and request ID
912        let image_id = compute_image_id(ECHO_ELF).unwrap();
913        let predicate = Predicate::digest_match(Journal::new(b"hello".to_vec()).digest());
914        let requirements = Requirements::new(image_id, predicate);
915        let request_id = RequestId::new(test_ctx.customer_signer.address(), 0);
916
917        // Zero cycles
918        let offer_params = OfferParams::default();
919        let offer_zero_mcycles =
920            layer.process((&requirements, &request_id, Some(0u64), &offer_params)).await?;
921        assert_eq!(offer_zero_mcycles.minPrice, U256::ZERO);
922        // Defaults from builder
923        assert_eq!(offer_zero_mcycles.rampUpPeriod, 60);
924        assert_eq!(offer_zero_mcycles.lockTimeout, 600);
925        assert_eq!(offer_zero_mcycles.timeout, 1200);
926        // Max price should be non-negative, to account for fixed costs.
927        assert!(offer_zero_mcycles.maxPrice > U256::ZERO);
928
929        // Now create an offer for 100 Mcycles.
930        let offer_more_mcycles =
931            layer.process((&requirements, &request_id, Some(100u64 << 20), &offer_params)).await?;
932        assert!(offer_more_mcycles.maxPrice > offer_zero_mcycles.maxPrice);
933
934        // Check that overrides are respected.
935        let min_price = U256::from(1u64);
936        let max_price = U256::from(5u64);
937        let offer_params = OfferParams::builder().max_price(max_price).min_price(min_price).into();
938        let offer_zero_mcycles =
939            layer.process((&requirements, &request_id, Some(0u64), &offer_params)).await?;
940        assert_eq!(offer_zero_mcycles.maxPrice, max_price);
941        assert_eq!(offer_zero_mcycles.minPrice, min_price);
942        assert_eq!(offer_zero_mcycles.rampUpPeriod, 60);
943        assert_eq!(offer_zero_mcycles.lockTimeout, 600);
944        assert_eq!(offer_zero_mcycles.timeout, 1200);
945        Ok(())
946    }
947
948    #[test]
949    fn request_params_with_program_url_infallible() {
950        // When passing a parsed URL, with_program_url should be infallible.
951        // NOTE: The `match *e {}` incantation is a compile-time assert that this error cannot
952        // occur.
953        let url = Url::parse("https://fileserver.example/guest.bin").unwrap();
954        RequestParams::new().with_program_url(url).inspect_err(|e| match *e {}).unwrap();
955    }
956
957    #[test]
958    fn request_params_with_input_url_infallible() {
959        // When passing a parsed URL, with_input_url should be infallible.
960        // NOTE: The `match *e {}` incantation is a compile-time assert that this error cannot
961        // occur.
962        let url = Url::parse("https://fileserver.example/input.bin").unwrap();
963        RequestParams::new().with_input_url(url).inspect_err(|e| match *e {}).unwrap();
964    }
965
966    #[test]
967    fn test_with_input_url() {
968        // Test with string URL
969        let params =
970            RequestParams::new().with_input_url("https://fileserver.example/input.bin").unwrap();
971
972        let input = params.request_input.unwrap();
973        assert_eq!(input.inputType, RequestInputType::Url);
974        assert_eq!(input.data.as_ref(), "https://fileserver.example/input.bin".as_bytes());
975
976        // Test with parsed URL
977        let url = Url::parse("https://fileserver.example/input2.bin").unwrap();
978        let params = RequestParams::new().with_input_url(url).unwrap();
979
980        let input = params.request_input.unwrap();
981        assert_eq!(input.inputType, RequestInputType::Url);
982        assert_eq!(input.data.as_ref(), "https://fileserver.example/input2.bin".as_bytes());
983    }
984
985    #[allow(dead_code)]
986    trait AssertSend: Send {}
987
988    // The StandardRequestBuilder must be Send such that a Client can be sent between threads.
989    impl AssertSend for StandardRequestBuilder<DynProvider, StandardStorageProvider> {}
990}