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        // TODO(risc0-ethereum/#597): This needs to be kept up to date with releases of
549        // risc0-ethereum.
550        let mut requirements = self.requirements;
551        requirements.selector = match risc0_zkvm::is_dev_mode() {
552            true => Some((Selector::FakeReceipt as u32).into()),
553            false => Some((Selector::Groth16V2_1 as u32).into()),
554        };
555        Self { requirements, ..self }
556    }
557}
558
559impl Debug for RequestParams {
560    /// [Debug] implementation that does not print the contents of the program.
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        f.debug_struct("ExampleRequestParams")
563            .field("program", &self.program.as_ref().map(|x| format!("[{} bytes]", x.len())))
564            .field("env", &self.env)
565            .field("program_url", &self.program_url)
566            .field("input", &self.request_input)
567            .field("cycles", &self.cycles)
568            .field("journal", &self.journal)
569            .field("request_id", &self.request_id)
570            .field("offer", &self.offer)
571            .field("requirements", &self.requirements)
572            .finish()
573    }
574}
575
576impl<Program, Env> From<(Program, Env)> for RequestParams
577where
578    Program: Into<Cow<'static, [u8]>>,
579    Env: Into<GuestEnv>,
580{
581    fn from(value: (Program, Env)) -> Self {
582        Self::default().with_program(value.0).with_env(value.1)
583    }
584}
585
586/// Error indicating that a required field is missing when building a request.
587///
588/// This error is returned when attempting to access a field that hasn't been
589/// set yet in the request parameters.
590#[derive(Debug)]
591pub struct MissingFieldError {
592    /// The name of the missing field.
593    pub label: Cow<'static, str>,
594    /// An optional hint as to the cause of the error, or how to resolve it.
595    pub hint: Option<Cow<'static, str>>,
596}
597
598impl Display for MissingFieldError {
599    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
600        match self.hint {
601            None => write!(f, "field `{}` is required but is uninitialized", self.label),
602            Some(ref hint) => {
603                write!(f, "field `{}` is required but is uninitialized; {hint}", self.label)
604            }
605        }
606    }
607}
608
609impl std::error::Error for MissingFieldError {}
610
611impl MissingFieldError {
612    /// Creates a new error for the specified missing field.
613    pub fn new(label: impl Into<Cow<'static, str>>) -> Self {
614        Self { label: label.into(), hint: None }
615    }
616
617    /// Creates a new error for the specified missing field.
618    pub fn with_hint(
619        label: impl Into<Cow<'static, str>>,
620        hint: impl Into<Cow<'static, str>>,
621    ) -> Self {
622        Self { label: label.into(), hint: Some(hint.into()) }
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    use std::sync::Arc;
629
630    use alloy::{
631        network::TransactionBuilder,
632        node_bindings::Anvil,
633        primitives::Address,
634        providers::{DynProvider, Provider},
635        rpc::types::TransactionRequest,
636    };
637    use boundless_market_test_utils::{create_test_ctx, ECHO_ELF};
638    use tracing_test::traced_test;
639    use url::Url;
640
641    use super::{
642        Layer, OfferLayer, OfferLayerConfig, OfferParams, PreflightLayer, RequestBuilder,
643        RequestId, RequestIdLayer, RequestIdLayerConfig, RequestIdLayerMode, RequestParams,
644        RequirementsLayer, StandardRequestBuilder, StorageLayer, StorageLayerConfig,
645    };
646
647    use crate::{
648        contracts::{
649            boundless_market::BoundlessMarketService, Predicate, RequestInput, RequestInputType,
650            Requirements,
651        },
652        input::GuestEnv,
653        storage::{fetch_url, MockStorageProvider, StorageProvider},
654        util::NotProvided,
655        StandardStorageProvider,
656    };
657    use alloy_primitives::U256;
658    use risc0_zkvm::{compute_image_id, sha::Digestible, Journal};
659
660    #[tokio::test]
661    #[traced_test]
662    async fn basic() -> anyhow::Result<()> {
663        let anvil = Anvil::new().spawn();
664        let test_ctx = create_test_ctx(&anvil).await.unwrap();
665        let storage = Arc::new(MockStorageProvider::start());
666        let market = BoundlessMarketService::new(
667            test_ctx.deployment.boundless_market_address,
668            test_ctx.customer_provider.clone(),
669            test_ctx.customer_signer.address(),
670        );
671
672        let request_builder = StandardRequestBuilder::builder()
673            .storage_layer(Some(storage))
674            .offer_layer(test_ctx.customer_provider.clone())
675            .request_id_layer(market)
676            .build()?;
677
678        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
679        let request = request_builder.build(params).await?;
680        println!("built request {request:#?}");
681        Ok(())
682    }
683
684    #[tokio::test]
685    #[traced_test]
686    async fn with_offer_layer_settings() -> anyhow::Result<()> {
687        let anvil = Anvil::new().spawn();
688        let test_ctx = create_test_ctx(&anvil).await.unwrap();
689        let storage = Arc::new(MockStorageProvider::start());
690        let market = BoundlessMarketService::new(
691            test_ctx.deployment.boundless_market_address,
692            test_ctx.customer_provider.clone(),
693            test_ctx.customer_signer.address(),
694        );
695
696        let request_builder = StandardRequestBuilder::builder()
697            .storage_layer(Some(storage))
698            .offer_layer(OfferLayer::new(
699                test_ctx.customer_provider.clone(),
700                OfferLayerConfig::builder().ramp_up_period(27).build()?,
701            ))
702            .request_id_layer(market)
703            .build()?;
704
705        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
706        let request = request_builder.build(params).await?;
707        assert_eq!(request.offer.rampUpPeriod, 27);
708        Ok(())
709    }
710
711    #[tokio::test]
712    #[traced_test]
713    async fn without_storage_provider() -> anyhow::Result<()> {
714        let anvil = Anvil::new().spawn();
715        let test_ctx = create_test_ctx(&anvil).await.unwrap();
716        let market = BoundlessMarketService::new(
717            test_ctx.deployment.boundless_market_address,
718            test_ctx.customer_provider.clone(),
719            test_ctx.customer_signer.address(),
720        );
721
722        let request_builder = StandardRequestBuilder::builder()
723            .storage_layer(None::<NotProvided>)
724            .offer_layer(test_ctx.customer_provider.clone())
725            .request_id_layer(market)
726            .build()?;
727
728        // Try building the reqeust by providing the program.
729        let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
730        let err = request_builder.build(params).await.unwrap_err();
731        tracing::debug!("err: {err}");
732
733        // Try again after uploading the program first.
734        let storage = Arc::new(MockStorageProvider::start());
735        let program_url = storage.upload_program(ECHO_ELF).await?;
736        let params = request_builder.params().with_program_url(program_url)?.with_stdin(b"hello!");
737        let request = request_builder.build(params).await?;
738        assert_eq!(
739            request.requirements.imageId,
740            risc0_zkvm::compute_image_id(ECHO_ELF)?.as_bytes()
741        );
742        Ok(())
743    }
744
745    #[tokio::test]
746    #[traced_test]
747    async fn test_storage_layer() -> anyhow::Result<()> {
748        let storage = Arc::new(MockStorageProvider::start());
749        let layer = StorageLayer::new(
750            Some(storage.clone()),
751            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
752        );
753        let env = GuestEnv::from_stdin(b"inline_data");
754        let (program_url, request_input) = layer.process((ECHO_ELF, &env)).await?;
755
756        // Program should be uploaded and input inline.
757        assert_eq!(fetch_url(&program_url).await?, ECHO_ELF);
758        assert_eq!(request_input.inputType, RequestInputType::Inline);
759        assert_eq!(request_input.data, env.encode()?);
760        Ok(())
761    }
762
763    #[tokio::test]
764    #[traced_test]
765    async fn test_storage_layer_no_provider() -> anyhow::Result<()> {
766        let layer = StorageLayer::<NotProvided>::from(
767            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
768        );
769
770        let env = GuestEnv::from_stdin(b"inline_data");
771        let request_input = layer.process(&env).await?;
772
773        // Program should be uploaded and input inline.
774        assert_eq!(request_input.inputType, RequestInputType::Inline);
775        assert_eq!(request_input.data, env.encode()?);
776        Ok(())
777    }
778
779    #[tokio::test]
780    #[traced_test]
781    async fn test_storage_layer_large_input() -> anyhow::Result<()> {
782        let storage = Arc::new(MockStorageProvider::start());
783        let layer = StorageLayer::new(
784            Some(storage.clone()),
785            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
786        );
787        let env = GuestEnv::from_stdin(rand::random_iter().take(2048).collect::<Vec<u8>>());
788        let (program_url, request_input) = layer.process((ECHO_ELF, &env)).await?;
789
790        // Program and input should be uploaded and input inline.
791        assert_eq!(fetch_url(&program_url).await?, ECHO_ELF);
792        assert_eq!(request_input.inputType, RequestInputType::Url);
793        let fetched_input = fetch_url(String::from_utf8(request_input.data.to_vec())?).await?;
794        assert_eq!(fetched_input, env.encode()?);
795        Ok(())
796    }
797
798    #[tokio::test]
799    #[traced_test]
800    async fn test_storage_layer_large_input_no_provider() -> anyhow::Result<()> {
801        let layer = StorageLayer::from(
802            StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
803        );
804
805        let env = GuestEnv::from_stdin(rand::random_iter().take(2048).collect::<Vec<u8>>());
806        let err = layer.process(&env).await.unwrap_err();
807
808        assert!(err
809            .to_string()
810            .contains("cannot upload input using StorageLayer with no storage_provider"));
811        Ok(())
812    }
813
814    #[tokio::test]
815    #[traced_test]
816    async fn test_preflight_layer() -> anyhow::Result<()> {
817        let storage = MockStorageProvider::start();
818        let program_url = storage.upload_program(ECHO_ELF).await?;
819        let layer = PreflightLayer::default();
820        let data = b"hello_zkvm".to_vec();
821        let env = GuestEnv::from_stdin(data.clone());
822        let input = RequestInput::inline(env.encode()?);
823        let session = layer.process((&program_url, &input)).await?;
824
825        assert_eq!(session.journal.as_ref(), data.as_slice());
826        // Verify non-zero cycle count and an exit code of zero.
827        let cycles: u64 = session.segments.iter().map(|s| 1 << s.po2).sum();
828        assert!(cycles > 0);
829        assert!(session.exit_code.is_ok());
830        Ok(())
831    }
832
833    #[tokio::test]
834    #[traced_test]
835    async fn test_requirements_layer() -> anyhow::Result<()> {
836        let layer = RequirementsLayer::default();
837        let program = ECHO_ELF;
838        let bytes = b"journal_data".to_vec();
839        let journal = Journal::new(bytes.clone());
840        let req = layer.process((program, &journal, &Default::default())).await?;
841
842        // Predicate should match the same journal
843        assert!(req.predicate.eval(&journal));
844        // And should not match different data
845        let other = Journal::new(b"other_data".to_vec());
846        assert!(!req.predicate.eval(&other));
847        Ok(())
848    }
849
850    #[tokio::test]
851    #[traced_test]
852    async fn test_request_id_layer_rand() -> anyhow::Result<()> {
853        let anvil = Anvil::new().spawn();
854        let test_ctx = create_test_ctx(&anvil).await?;
855        let market = BoundlessMarketService::new(
856            test_ctx.deployment.boundless_market_address,
857            test_ctx.customer_provider.clone(),
858            test_ctx.customer_signer.address(),
859        );
860        let layer = RequestIdLayer::from(market.clone());
861        assert_eq!(layer.config.mode, RequestIdLayerMode::Rand);
862        let id = layer.process(()).await?;
863        assert_eq!(id.addr, test_ctx.customer_signer.address());
864        assert!(!id.smart_contract_signed);
865        Ok(())
866    }
867
868    #[tokio::test]
869    #[traced_test]
870    async fn test_request_id_layer_nonce() -> anyhow::Result<()> {
871        let anvil = Anvil::new().spawn();
872        let test_ctx = create_test_ctx(&anvil).await?;
873        let market = BoundlessMarketService::new(
874            test_ctx.deployment.boundless_market_address,
875            test_ctx.customer_provider.clone(),
876            test_ctx.customer_signer.address(),
877        );
878        let layer = RequestIdLayer::new(
879            market.clone(),
880            RequestIdLayerConfig::builder().mode(RequestIdLayerMode::Nonce).build()?,
881        );
882
883        let id = layer.process(()).await?;
884        assert_eq!(id.addr, test_ctx.customer_signer.address());
885        // The customer address has sent no transactions.
886        assert_eq!(id.index, 0);
887        assert!(!id.smart_contract_signed);
888
889        // Send a tx then check that the index increments.
890        let tx = TransactionRequest::default()
891            .with_from(test_ctx.customer_signer.address())
892            .with_to(Address::ZERO)
893            .with_value(U256::from(1));
894        test_ctx.customer_provider.send_transaction(tx).await?.watch().await?;
895
896        let id = layer.process(()).await?;
897        assert_eq!(id.addr, test_ctx.customer_signer.address());
898        // The customer address has sent one transaction.
899        assert_eq!(id.index, 1);
900        assert!(!id.smart_contract_signed);
901
902        Ok(())
903    }
904
905    #[tokio::test]
906    #[traced_test]
907    async fn test_offer_layer_estimates() -> anyhow::Result<()> {
908        // Use Anvil-backed provider for gas price
909        let anvil = Anvil::new().spawn();
910        let test_ctx = create_test_ctx(&anvil).await?;
911        let provider = test_ctx.customer_provider.clone();
912        let layer = OfferLayer::from(provider.clone());
913        // Build minimal requirements and request ID
914        let image_id = compute_image_id(ECHO_ELF).unwrap();
915        let predicate = Predicate::digest_match(Journal::new(b"hello".to_vec()).digest());
916        let requirements = Requirements::new(image_id, predicate);
917        let request_id = RequestId::new(test_ctx.customer_signer.address(), 0);
918
919        // Zero cycles
920        let offer_params = OfferParams::default();
921        let offer_zero_mcycles =
922            layer.process((&requirements, &request_id, Some(0u64), &offer_params)).await?;
923        assert_eq!(offer_zero_mcycles.minPrice, U256::ZERO);
924        // Defaults from builder
925        assert_eq!(offer_zero_mcycles.rampUpPeriod, 60);
926        assert_eq!(offer_zero_mcycles.lockTimeout, 600);
927        assert_eq!(offer_zero_mcycles.timeout, 1200);
928        // Max price should be non-negative, to account for fixed costs.
929        assert!(offer_zero_mcycles.maxPrice > U256::ZERO);
930
931        // Now create an offer for 100 Mcycles.
932        let offer_more_mcycles =
933            layer.process((&requirements, &request_id, Some(100u64 << 20), &offer_params)).await?;
934        assert!(offer_more_mcycles.maxPrice > offer_zero_mcycles.maxPrice);
935
936        // Check that overrides are respected.
937        let min_price = U256::from(1u64);
938        let max_price = U256::from(5u64);
939        let offer_params = OfferParams::builder().max_price(max_price).min_price(min_price).into();
940        let offer_zero_mcycles =
941            layer.process((&requirements, &request_id, Some(0u64), &offer_params)).await?;
942        assert_eq!(offer_zero_mcycles.maxPrice, max_price);
943        assert_eq!(offer_zero_mcycles.minPrice, min_price);
944        assert_eq!(offer_zero_mcycles.rampUpPeriod, 60);
945        assert_eq!(offer_zero_mcycles.lockTimeout, 600);
946        assert_eq!(offer_zero_mcycles.timeout, 1200);
947        Ok(())
948    }
949
950    #[test]
951    fn request_params_with_program_url_infallible() {
952        // When passing a parsed URL, with_program_url should be infallible.
953        // NOTE: The `match *e {}` incantation is a compile-time assert that this error cannot
954        // occur.
955        let url = Url::parse("https://fileserver.example/guest.bin").unwrap();
956        RequestParams::new().with_program_url(url).inspect_err(|e| match *e {}).unwrap();
957    }
958
959    #[test]
960    fn request_params_with_input_url_infallible() {
961        // When passing a parsed URL, with_input_url should be infallible.
962        // NOTE: The `match *e {}` incantation is a compile-time assert that this error cannot
963        // occur.
964        let url = Url::parse("https://fileserver.example/input.bin").unwrap();
965        RequestParams::new().with_input_url(url).inspect_err(|e| match *e {}).unwrap();
966    }
967
968    #[test]
969    fn test_with_input_url() {
970        // Test with string URL
971        let params =
972            RequestParams::new().with_input_url("https://fileserver.example/input.bin").unwrap();
973
974        let input = params.request_input.unwrap();
975        assert_eq!(input.inputType, RequestInputType::Url);
976        assert_eq!(input.data.as_ref(), "https://fileserver.example/input.bin".as_bytes());
977
978        // Test with parsed URL
979        let url = Url::parse("https://fileserver.example/input2.bin").unwrap();
980        let params = RequestParams::new().with_input_url(url).unwrap();
981
982        let input = params.request_input.unwrap();
983        assert_eq!(input.inputType, RequestInputType::Url);
984        assert_eq!(input.data.as_ref(), "https://fileserver.example/input2.bin".as_bytes());
985    }
986
987    #[allow(dead_code)]
988    trait AssertSend: Send {}
989
990    // The StandardRequestBuilder must be Send such that a Client can be sent between threads.
991    impl AssertSend for StandardRequestBuilder<DynProvider, StandardStorageProvider> {}
992}