1#![deny(missing_docs)]
18
19use alloy::{primitives::Address, sol_types::SolStruct};
20use anyhow::{bail, Context, Result};
21use boundless_assessor::{AssessorInput, Fulfillment};
22use risc0_aggregation::{
23 merkle_path, GuestState, SetInclusionReceipt, SetInclusionReceiptVerifierParameters,
24};
25use risc0_ethereum_contracts::encode_seal;
26use risc0_zkvm::{
27 compute_image_id, default_prover,
28 sha::{Digest, Digestible},
29 ExecutorEnv, ProverOpts, Receipt, ReceiptClaim,
30};
31use url::Url;
32
33use boundless_market::{
34 contracts::{EIP721DomainSaltless, Fulfillment as BoundlessFulfillment, InputType},
35 input::GuestEnv,
36 order_stream_client::Order,
37};
38
39alloy::sol!(
40 #[sol(all_derives)]
41 struct OrderFulfilled {
43 bytes32 root;
45 bytes seal;
47 BoundlessFulfillment[] fills;
49 bytes assessorSeal;
51 address prover;
53 }
54);
55
56impl OrderFulfilled {
57 pub fn new(
59 fill: BoundlessFulfillment,
60 root_receipt: Receipt,
61 assessor_receipt: SetInclusionReceipt<ReceiptClaim>,
62 prover: Address,
63 ) -> Result<Self> {
64 let state = GuestState::decode(&root_receipt.journal.bytes)?;
65 let root = state.mmr.finalized_root().context("failed to get finalized root")?;
66
67 let root_seal = encode_seal(&root_receipt)?;
68 let assessor_seal = assessor_receipt.abi_encode_seal()?;
69
70 Ok(OrderFulfilled {
71 root: <[u8; 32]>::from(root).into(),
72 seal: root_seal.into(),
73 fills: vec![fill],
74 assessorSeal: assessor_seal.into(),
75 prover,
76 })
77 }
78}
79
80pub async fn fetch_url(url_str: &str) -> Result<Vec<u8>> {
83 tracing::debug!("Fetching URL: {}", url_str);
84 let url = Url::parse(url_str)?;
85
86 match url.scheme() {
87 "http" | "https" => fetch_http(&url).await,
88 "file" => fetch_file(&url).await,
89 _ => bail!("unsupported URL scheme: {}", url.scheme()),
90 }
91}
92
93async fn fetch_http(url: &Url) -> Result<Vec<u8>> {
94 let response = reqwest::get(url.as_str()).await?;
95 let status = response.status();
96 if !status.is_success() {
97 bail!("HTTP request failed with status: {}", status);
98 }
99
100 Ok(response.bytes().await?.to_vec())
101}
102
103async fn fetch_file(url: &Url) -> Result<Vec<u8>> {
104 let path = std::path::Path::new(url.path());
105 let data = tokio::fs::read(path).await?;
106 Ok(data)
107}
108
109pub struct DefaultProver {
128 set_builder_elf: Vec<u8>,
129 set_builder_image_id: Digest,
130 assessor_elf: Vec<u8>,
131 address: Address,
132 domain: EIP721DomainSaltless,
133}
134
135impl DefaultProver {
136 pub fn new(
138 set_builder_elf: Vec<u8>,
139 assessor_elf: Vec<u8>,
140 address: Address,
141 domain: EIP721DomainSaltless,
142 ) -> Result<Self> {
143 let set_builder_image_id = compute_image_id(&set_builder_elf)?;
144 Ok(Self { set_builder_elf, set_builder_image_id, assessor_elf, address, domain })
145 }
146
147 pub(crate) async fn prove(
150 &self,
151 elf: Vec<u8>,
152 input: Vec<u8>,
153 assumptions: Vec<Receipt>,
154 opts: ProverOpts,
155 ) -> Result<Receipt> {
156 let receipt = tokio::task::spawn_blocking(move || {
157 let mut env = ExecutorEnv::builder();
158 env.write_slice(&input);
159 for assumption_receipt in assumptions.iter() {
160 env.add_assumption(assumption_receipt.clone());
161 }
162 let env = env.build()?;
163 default_prover().prove_with_opts(env, &elf, &opts)
164 })
165 .await??
166 .receipt;
167 Ok(receipt)
168 }
169
170 pub(crate) async fn finalize(
172 &self,
173 claims: Vec<ReceiptClaim>,
174 assumptions: Vec<Receipt>,
175 ) -> Result<Receipt> {
176 let input = GuestState::initial(self.set_builder_image_id)
177 .into_input(claims, true)
178 .context("Failed to build set builder input")?;
179 let encoded_input = bytemuck::pod_collect_to_vec(&risc0_zkvm::serde::to_vec(&input)?);
180
181 self.prove(self.set_builder_elf.clone(), encoded_input, assumptions, ProverOpts::succinct())
182 .await
183 }
184
185 pub(crate) async fn assessor(
187 &self,
188 fills: Vec<Fulfillment>,
189 receipts: Vec<Receipt>,
190 ) -> Result<Receipt> {
191 let assessor_input =
192 AssessorInput { domain: self.domain.clone(), fills, prover_address: self.address };
193 self.prove(
194 self.assessor_elf.clone(),
195 assessor_input.to_vec(),
196 receipts,
197 ProverOpts::succinct(),
198 )
199 .await
200 }
201
202 pub async fn fulfill(
208 &self,
209 order: Order,
210 require_payment: bool,
211 ) -> Result<(
212 BoundlessFulfillment,
213 Receipt,
214 SetInclusionReceipt<ReceiptClaim>,
215 SetInclusionReceipt<ReceiptClaim>,
216 )> {
217 let request = order.request.clone();
218 let order_elf = fetch_url(&request.imageUrl).await?;
219 let order_input: Vec<u8> = match request.input.inputType {
220 InputType::Inline => GuestEnv::decode(&request.input.data)?.stdin,
221 InputType::Url => {
222 GuestEnv::decode(
223 &fetch_url(
224 std::str::from_utf8(&request.input.data)
225 .context("input url is not utf8")?,
226 )
227 .await?,
228 )?
229 .stdin
230 }
231 _ => bail!("Unsupported input type"),
232 };
233 let order_receipt =
234 self.prove(order_elf.clone(), order_input, vec![], ProverOpts::succinct()).await?;
235 let order_journal = order_receipt.journal.bytes.clone();
236 let order_image_id = compute_image_id(&order_elf)?;
237
238 let fill = Fulfillment {
239 request: order.request.clone(),
240 signature: order.signature.into(),
241 journal: order_journal.clone(),
242 require_payment,
243 };
244
245 let assessor_receipt = self.assessor(vec![fill], vec![order_receipt.clone()]).await?;
246 let assessor_journal = assessor_receipt.journal.bytes.clone();
247 let assessor_image_id = compute_image_id(&self.assessor_elf)?;
248
249 let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
250 let order_claim_digest = order_claim.digest();
251 let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal);
252 let assessor_claim_digest = assessor_claim.digest();
253 let root_receipt = self
254 .finalize(
255 vec![order_claim.clone(), assessor_claim.clone()],
256 vec![order_receipt, assessor_receipt],
257 )
258 .await?;
259
260 let order_path = merkle_path(&[order_claim_digest, assessor_claim_digest], 0);
261 let assessor_path = merkle_path(&[order_claim_digest, assessor_claim_digest], 1);
262
263 let verifier_parameters =
264 SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
265
266 let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
267 order_claim,
268 order_path,
269 verifier_parameters.digest(),
270 );
271 let order_seal = order_inclusion_receipt.abi_encode_seal()?;
272
273 let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
274 assessor_claim,
275 assessor_path,
276 verifier_parameters.digest(),
277 );
278
279 let fulfillment = BoundlessFulfillment {
280 id: request.id,
281 requestDigest: order.request.eip712_signing_hash(&self.domain.alloy_struct()),
282 imageId: request.requirements.imageId,
283 journal: order_journal.into(),
284 requirePayment: require_payment,
285 seal: order_seal.into(),
286 };
287
288 Ok((fulfillment, root_receipt, order_inclusion_receipt, assessor_inclusion_receipt))
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use alloy::{primitives::PrimitiveSignature, signers::local::PrivateKeySigner};
296 use boundless_market::contracts::{
297 eip712_domain, Input, Offer, Predicate, ProofRequest, Requirements,
298 };
299 use guest_assessor::ASSESSOR_GUEST_ELF;
300 use guest_set_builder::SET_BUILDER_ELF;
301 use guest_util::{ECHO_ID, ECHO_PATH};
302 use risc0_zkvm::VerifierContext;
303
304 async fn setup_proving_request_and_signature(
305 signer: &PrivateKeySigner,
306 ) -> (ProofRequest, PrimitiveSignature) {
307 let request = ProofRequest::new(
308 0,
309 &signer.address(),
310 Requirements {
311 imageId: <[u8; 32]>::from(Digest::from(ECHO_ID)).into(),
312 predicate: Predicate::prefix_match(vec![1]),
313 },
314 format!("file://{ECHO_PATH}"),
315 Input::inline(vec![1, 2, 3, 4]),
316 Offer::default(),
317 );
318
319 let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
320 (request, signature)
321 }
322
323 #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
324 #[tokio::test]
325 async fn test_fulfill() {
326 let signer = PrivateKeySigner::random();
327 let (request, signature) = setup_proving_request_and_signature(&signer).await;
328
329 let domain = eip712_domain(Address::ZERO, 1);
330 let prover = DefaultProver::new(
331 SET_BUILDER_ELF.to_vec(),
332 ASSESSOR_GUEST_ELF.to_vec(),
333 Address::ZERO,
334 domain,
335 )
336 .expect("failed to create prover");
337
338 let order = Order { request, signature };
339 let (_, root_receipt, order_receipt, assessor_receipt) =
340 prover.fulfill(order.clone(), false).await.unwrap();
341
342 let verifier_parameters =
343 SetInclusionReceiptVerifierParameters { image_id: prover.set_builder_image_id };
344
345 order_receipt
346 .with_root(root_receipt.clone())
347 .verify_integrity_with_context(
348 &VerifierContext::default(),
349 verifier_parameters.clone(),
350 None,
351 )
352 .unwrap();
353 assessor_receipt
354 .with_root(root_receipt.clone())
355 .verify_integrity_with_context(&VerifierContext::default(), verifier_parameters, None)
356 .unwrap();
357 }
358}