Skip to main content

boundless_market/request_builder/
requirements_layer.rs

1// Copyright 2026 Boundless Foundation, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{Adapt, Layer, MissingFieldError, RequestParams};
16#[cfg(feature = "blake3-groth16")]
17use crate::blake3_groth16;
18use crate::contracts::{Callback, Predicate, Requirements};
19#[cfg(feature = "blake3-groth16")]
20use crate::selector::is_blake3_groth16_selector;
21use alloy::primitives::{aliases::U96, Address, FixedBytes, B256};
22use anyhow::{ensure, Context};
23use clap::Args;
24use derive_builder::Builder;
25use risc0_zkvm::{compute_image_id, Journal};
26use risc0_zkvm::{sha::Digestible, Digest};
27
28const DEFAULT_CALLBACK_GAS_LIMT: u64 = 100000u64;
29
30/// A layer responsible for configuring verification requirements for proof requests.
31///
32/// This layer sets up the predicate, image ID, callbacks, and other verification
33/// parameters that ensure proofs meet the requestor's specifications.
34#[non_exhaustive]
35#[derive(Clone, Builder, Default)]
36pub struct RequirementsLayer {}
37
38#[non_exhaustive]
39#[derive(Clone, Debug, Default, Builder, Args)]
40/// A partial [Requirements], with all the fields as optional. Used in the [RequirementsLayer] to
41/// provide explicit settings.
42///
43/// Does not include the predicate, which is created by [RequirementsLayer].
44pub struct RequirementParams {
45    /// Predicate specifying what conditions the proof must satisfy.
46    #[clap(skip)]
47    #[builder(setter(strip_option, into), default)]
48    pub predicate: Option<Predicate>,
49
50    /// Image ID identifying the program to be executed.
51    #[clap(long)]
52    #[builder(setter(strip_option, into), default)]
53    pub image_id: Option<B256>,
54
55    /// Address of the contract to call when the proof is fulfilled.
56    #[clap(long)]
57    #[builder(setter(strip_option, into), default)]
58    pub callback_address: Option<Address>,
59
60    /// Gas limit for the callback when the proof is fulfilled.
61    #[clap(long)]
62    #[builder(setter(strip_option), default)]
63    pub callback_gas_limit: Option<u64>,
64
65    /// Selector specifying the type of proof required.
66    #[clap(long)]
67    #[builder(setter(strip_option, into), default)]
68    pub selector: Option<FixedBytes<4>>,
69}
70
71impl TryFrom<Requirements> for RequirementParams {
72    type Error = anyhow::Error;
73
74    fn try_from(value: Requirements) -> Result<Self, Self::Error> {
75        let predicate = Predicate::try_from(value.predicate)?;
76        let image_id = predicate.image_id().map(<[u8; 32]>::from).map(Into::into);
77        Ok(Self {
78            predicate: Some(predicate),
79            selector: Some(value.selector),
80            callback_address: Some(value.callback.addr),
81            callback_gas_limit: Some(value.callback.gasLimit.to()),
82            image_id,
83        })
84    }
85}
86
87impl TryFrom<RequirementParams> for Requirements {
88    type Error = MissingFieldError;
89
90    fn try_from(value: RequirementParams) -> Result<Self, Self::Error> {
91        Ok(Self {
92            predicate: value
93                .predicate
94                .ok_or(MissingFieldError::with_hint(
95                    "predicate",
96                    "please provide a Predicate with requirements e.g. a digest match on a journal",
97                ))?
98                .into(),
99            selector: value.selector.unwrap_or_default(),
100            callback: Callback {
101                addr: value.callback_address.unwrap_or_default(),
102                gasLimit: U96::from(value.callback_gas_limit.unwrap_or_default()),
103            },
104        })
105    }
106}
107
108impl From<RequirementParamsBuilder> for RequirementParams {
109    fn from(value: RequirementParamsBuilder) -> Self {
110        // Builder should be infallible.
111        value.build().expect("implementation error in RequirementParams")
112    }
113}
114
115// Allows for a nicer builder pattern in RequestParams.
116impl From<&mut RequirementParamsBuilder> for RequirementParams {
117    fn from(value: &mut RequirementParamsBuilder) -> Self {
118        value.clone().into()
119    }
120}
121
122impl RequirementParams {
123    /// Creates a new builder for constructing [RequirementParams].
124    ///
125    /// Use this to set specific verification requirements for the proof request.
126    pub fn builder() -> RequirementParamsBuilder {
127        Default::default()
128    }
129}
130
131impl RequirementsLayer {
132    /// Creates a new builder for constructing a [RequirementsLayer].
133    ///
134    /// The requirements layer configures verification parameters for the proof request.
135    pub fn builder() -> RequirementsLayerBuilder {
136        Default::default()
137    }
138}
139
140impl Layer<(&[u8], &Journal, &RequirementParams)> for RequirementsLayer {
141    type Output = Requirements;
142    type Error = anyhow::Error;
143
144    async fn process(
145        &self,
146        (program, journal, params): (&[u8], &Journal, &RequirementParams),
147    ) -> Result<Self::Output, Self::Error> {
148        let image_id =
149            compute_image_id(program).context("failed to compute image ID for program")?;
150        self.process((image_id, journal, params)).await
151    }
152}
153
154impl Layer<(Digest, &Journal, &RequirementParams)> for RequirementsLayer {
155    type Output = Requirements;
156    type Error = anyhow::Error;
157
158    async fn process(
159        &self,
160        (image_id, journal, params): (Digest, &Journal, &RequirementParams),
161    ) -> Result<Self::Output, Self::Error> {
162        #[allow(unused_mut)]
163        let mut predicate = params.predicate.clone();
164        #[cfg(feature = "blake3-groth16")]
165        if let Some(selector) = &params.selector {
166            if is_blake3_groth16_selector(*selector) {
167                if journal.bytes.len() != 32 {
168                    anyhow::bail!(
169                        "Blake3Groth16 proofs require a 32-byte journal, got {} bytes",
170                        journal.bytes.len()
171                    );
172                }
173                if let Some(pred) = &predicate {
174                    matches!(pred, Predicate::ClaimDigestMatch(_)).then_some(()).ok_or_else(
175                        || {
176                            anyhow::anyhow!(
177                                "Blake3Groth16 proofs require a ClaimDigestMatch predicate"
178                            )
179                        },
180                    )?;
181                } else {
182                    predicate = Some(params.predicate.clone().unwrap_or_else(|| {
183                        let blake3_claim_digest = blake3_groth16::Blake3Groth16ReceiptClaim::ok(
184                            image_id,
185                            journal.bytes.clone(),
186                        )
187                        .digest();
188                        Predicate::claim_digest_match(blake3_claim_digest)
189                    }));
190                }
191            }
192        }
193
194        let predicate =
195            predicate.unwrap_or_else(|| Predicate::digest_match(image_id, journal.digest()));
196
197        if let Some(params_image_id) = params.image_id {
198            ensure!(
199                image_id == Digest::from(<[u8; 32]>::from(params_image_id)),
200                "mismatch between specified and computed image ID"
201            )
202        }
203        let callback = params
204            .callback_address
205            .map(|addr| Callback {
206                addr,
207                gasLimit: U96::from(params.callback_gas_limit.unwrap_or(DEFAULT_CALLBACK_GAS_LIMT)),
208            })
209            .unwrap_or_default();
210
211        let selector = params.selector.unwrap_or_default();
212        Ok(Requirements { predicate: predicate.into(), callback, selector })
213    }
214}
215
216impl Adapt<RequirementsLayer> for RequestParams {
217    type Output = RequestParams;
218    type Error = anyhow::Error;
219
220    async fn process_with(self, layer: &RequirementsLayer) -> Result<Self::Output, Self::Error> {
221        tracing::trace!("Processing {self:?} with RequirementsLayer");
222
223        // If the two required paramters of image ID and predicate are already set, skip this
224        // layer.
225        if self.requirements.predicate.is_some() && self.requirements.image_id.is_some() {
226            return Ok(self);
227        }
228
229        let journal = self.require_journal().context("failed to build Requirements for request")?;
230        let requirements = if let Some(image_id) = self.image_id {
231            layer.process((image_id, journal, &self.requirements)).await?
232        } else {
233            let program = self.require_program()?;
234            layer.process((program, journal, &self.requirements)).await?
235        };
236
237        Ok(self.with_requirements(RequirementParams::try_from(requirements)?))
238    }
239}