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