Skip to main content

boundless_market/request_builder/
finalizer.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, RequestParams};
16use crate::{
17    contracts::{
18        FulfillmentData, Offer, Predicate, PredicateType, ProofRequest, RequestId, RequestInput,
19        Requirements,
20    },
21    selector::is_blake3_groth16_selector,
22    util::now_timestamp,
23};
24use anyhow::{bail, Context};
25use derive_builder::Builder;
26use url::Url;
27
28#[non_exhaustive]
29#[derive(Debug, Clone, Builder)]
30/// Configuration for the [Finalizer] layer.
31///
32/// Controls validation behavior when finalizing a proof request.
33pub struct FinalizerConfig {
34    /// If true, the request's expiration time will be checked against the system clock.
35    #[builder(default = true)]
36    pub check_expiration: bool,
37}
38
39/// The final layer in the request building pipeline.
40///
41/// This layer assembles the complete [ProofRequest] from its components and performs
42/// validation checks to ensure the request is valid before it is submitted.
43#[non_exhaustive]
44#[derive(Debug, Clone, Default)]
45pub struct Finalizer {
46    /// Configuration controlling the finalization process.
47    pub config: FinalizerConfig,
48}
49
50impl From<FinalizerConfig> for Finalizer {
51    fn from(config: FinalizerConfig) -> Self {
52        Self { config }
53    }
54}
55
56impl Default for FinalizerConfig {
57    fn default() -> Self {
58        Self::builder().build().expect("implementation error in Default for FinalizerConfig")
59    }
60}
61
62impl FinalizerConfig {
63    /// Creates a new builder for constructing a [FinalizerConfig].
64    ///
65    /// This provides a fluent API for configuring the finalizer behavior.
66    pub fn builder() -> FinalizerConfigBuilder {
67        Default::default()
68    }
69}
70
71impl Layer<(Url, RequestInput, Requirements, Offer, RequestId)> for Finalizer {
72    type Output = ProofRequest;
73    type Error = anyhow::Error;
74
75    async fn process(
76        &self,
77        (program_url, input, requirements, offer, request_id): (
78            Url,
79            RequestInput,
80            Requirements,
81            Offer,
82            RequestId,
83        ),
84    ) -> Result<Self::Output, Self::Error> {
85        let request = ProofRequest {
86            requirements,
87            id: request_id.into(),
88            imageUrl: program_url.into(),
89            input,
90            offer,
91        };
92
93        request.validate().context("built request is invalid; check request parameters")?;
94        if self.config.check_expiration && request.is_expired() {
95            bail!(
96                "request expired at {}; current time is {}",
97                request.expires_at(),
98                now_timestamp()
99            );
100        }
101        if self.config.check_expiration && request.is_lock_expired() {
102            bail!(
103                "request lock expired at {}; current time is {}",
104                request.lock_expires_at(),
105                now_timestamp()
106            );
107        }
108        Ok(request)
109    }
110}
111
112impl Adapt<Finalizer> for RequestParams {
113    type Output = ProofRequest;
114    type Error = anyhow::Error;
115
116    async fn process_with(self, layer: &Finalizer) -> Result<Self::Output, Self::Error> {
117        tracing::trace!("Processing {self:?} with Finalizer");
118
119        // We create local variables to hold owned values
120        let program_url = self.require_program_url().context("failed to build request")?.clone();
121        let input = self.require_request_input().context("failed to build request")?.clone();
122        let requirements: Requirements = self
123            .requirements
124            .clone()
125            .try_into()
126            .context("failed to build request: requirements are incomplete")?;
127        let offer: Offer = self
128            .offer
129            .clone()
130            .try_into()
131            .context("failed to build request: offer is incomplete")?;
132        let request_id = self.require_request_id().context("failed to build request")?.clone();
133
134        // If enough data is provided, check that the known journal and image match the predicate.
135        let predicate = Predicate::try_from(requirements.predicate.clone())?;
136        let eval = match (&self.journal, self.image_id) {
137            (Some(journal), Some(image_id)) => {
138                tracing::debug!("Evaluating journal and image id against predicate ");
139                let eval_data = if is_blake3_groth16_selector(requirements.selector) {
140                    if requirements.predicate.predicateType != PredicateType::ClaimDigestMatch {
141                        bail!("Blake3Groth16 proofs require a ClaimDigestMatch predicate");
142                    }
143                    if journal.bytes.len() != 32 {
144                        bail!(
145                            "Blake3Groth16 proofs require a 32-byte journal, got {} bytes",
146                            journal.bytes.len()
147                        );
148                    }
149                    // It is not possible to fulfill a blake3 groth16 request with fulfillment data
150                    FulfillmentData::None
151                } else {
152                    FulfillmentData::from_image_id_and_journal(image_id, journal.bytes.clone())
153                };
154                predicate.eval(&eval_data).is_some()
155            }
156            // Do not run the check.
157            _ => true,
158        };
159        if !eval {
160            bail!("journal in request builder does not match requirements predicate; check request parameters.\npredicate = {:?}\njournal = {:?}", predicate, self.journal.as_ref().map(hex::encode));
161        }
162
163        layer.process((program_url, input, requirements, offer, request_id)).await
164    }
165}