1#![deny(missing_docs)]
18
19use alloy::{
20 primitives::Address,
21 sol_types::{SolStruct, SolValue},
22};
23use anyhow::{bail, Context, Result};
24use bonsai_sdk::non_blocking::Client as BonsaiClient;
25use boundless_assessor::{AssessorInput, Fulfillment};
26use chrono::{DateTime, Local};
27use risc0_aggregation::{
28 merkle_path, GuestState, SetInclusionReceipt, SetInclusionReceiptVerifierParameters,
29};
30use risc0_ethereum_contracts::encode_seal;
31use risc0_zkvm::{
32 compute_image_id, default_prover, is_dev_mode,
33 sha::{Digest, Digestible},
34 ExecutorEnv, ProverOpts, Receipt, ReceiptClaim,
35};
36use url::Url;
37
38use boundless_market::{
39 contracts::{
40 AssessorJournal, AssessorReceipt, EIP712DomainSaltless,
41 Fulfillment as BoundlessFulfillment, InputType,
42 },
43 input::{GuestEnv, InputBuilder},
44 order_stream_client::Order,
45 selector::{is_groth16_selector, SupportedSelectors},
46};
47
48alloy::sol!(
49 #[sol(all_derives)]
50 struct OrderFulfilled {
52 bytes32 root;
54 bytes seal;
56 BoundlessFulfillment[] fills;
58 AssessorReceipt assessorReceipt;
60 }
61);
62
63impl OrderFulfilled {
64 pub fn new(
66 fills: Vec<BoundlessFulfillment>,
67 root_receipt: Receipt,
68 assessor_receipt: AssessorReceipt,
69 ) -> Result<Self> {
70 let state = GuestState::decode(&root_receipt.journal.bytes)?;
71 let root = state.mmr.finalized_root().context("failed to get finalized root")?;
72
73 let root_seal = encode_seal(&root_receipt)?;
74
75 Ok(OrderFulfilled {
76 root: <[u8; 32]>::from(root).into(),
77 seal: root_seal.into(),
78 fills,
79 assessorReceipt: assessor_receipt,
80 })
81 }
82}
83
84pub fn convert_timestamp(timestamp: u64) -> DateTime<Local> {
86 let t = DateTime::from_timestamp(timestamp as i64, 0).expect("invalid timestamp");
87 t.with_timezone(&Local)
88}
89
90pub async fn fetch_url(url_str: &str) -> Result<Vec<u8>> {
93 tracing::debug!("Fetching URL: {}", url_str);
94 let url = Url::parse(url_str)?;
95
96 match url.scheme() {
97 "http" | "https" => fetch_http(&url).await,
98 "file" => fetch_file(&url).await,
99 _ => bail!("unsupported URL scheme: {}", url.scheme()),
100 }
101}
102
103async fn fetch_http(url: &Url) -> Result<Vec<u8>> {
104 let response = reqwest::get(url.as_str()).await?;
105 let status = response.status();
106 if !status.is_success() {
107 bail!("HTTP request failed with status: {}", status);
108 }
109
110 Ok(response.bytes().await?.to_vec())
111}
112
113async fn fetch_file(url: &Url) -> Result<Vec<u8>> {
114 let path = std::path::Path::new(url.path());
115 let data = tokio::fs::read(path).await?;
116 Ok(data)
117}
118
119pub struct DefaultProver {
138 set_builder_program: Vec<u8>,
139 set_builder_image_id: Digest,
140 assessor_program: Vec<u8>,
141 address: Address,
142 domain: EIP712DomainSaltless,
143 supported_selectors: SupportedSelectors,
144}
145
146impl DefaultProver {
147 pub fn new(
149 set_builder_program: Vec<u8>,
150 assessor_program: Vec<u8>,
151 address: Address,
152 domain: EIP712DomainSaltless,
153 ) -> Result<Self> {
154 let set_builder_image_id = compute_image_id(&set_builder_program)?;
155 let supported_selectors =
156 SupportedSelectors::default().with_set_builder_image_id(set_builder_image_id);
157 Ok(Self {
158 set_builder_program,
159 set_builder_image_id,
160 assessor_program,
161 address,
162 domain,
163 supported_selectors,
164 })
165 }
166
167 pub(crate) async fn prove(
170 &self,
171 program: Vec<u8>,
172 input: Vec<u8>,
173 assumptions: Vec<Receipt>,
174 opts: ProverOpts,
175 ) -> Result<Receipt> {
176 let receipt = tokio::task::spawn_blocking(move || {
177 let mut env = ExecutorEnv::builder();
178 env.write_slice(&input);
179 for assumption_receipt in assumptions.iter() {
180 env.add_assumption(assumption_receipt.clone());
181 }
182 let env = env.build()?;
183
184 default_prover().prove_with_opts(env, &program, &opts)
185 })
186 .await??
187 .receipt;
188 Ok(receipt)
189 }
190
191 pub(crate) async fn compress(&self, succinct_receipt: &Receipt) -> Result<Receipt> {
192 let prover = default_prover();
193 if prover.get_name() == "bonsai" {
194 return compress_with_bonsai(succinct_receipt).await;
195 }
196 if is_dev_mode() {
197 return Ok(succinct_receipt.clone());
198 }
199
200 let receipt = succinct_receipt.clone();
201 tokio::task::spawn_blocking(move || {
202 default_prover().compress(&ProverOpts::groth16(), &receipt)
203 })
204 .await?
205 }
206
207 pub(crate) async fn finalize(
209 &self,
210 claims: Vec<ReceiptClaim>,
211 assumptions: Vec<Receipt>,
212 ) -> Result<Receipt> {
213 let input = GuestState::initial(self.set_builder_image_id)
214 .into_input(claims, true)
215 .context("Failed to build set builder input")?;
216 let encoded_input = bytemuck::pod_collect_to_vec(&risc0_zkvm::serde::to_vec(&input)?);
217
218 self.prove(
219 self.set_builder_program.clone(),
220 encoded_input,
221 assumptions,
222 ProverOpts::groth16(),
223 )
224 .await
225 }
226
227 pub(crate) async fn assessor(
229 &self,
230 fills: Vec<Fulfillment>,
231 receipts: Vec<Receipt>,
232 ) -> Result<Receipt> {
233 let assessor_input =
234 AssessorInput { domain: self.domain.clone(), fills, prover_address: self.address };
235
236 let stdin = InputBuilder::new().write_frame(&assessor_input.encode()).stdin;
237
238 self.prove(self.assessor_program.clone(), stdin, receipts, ProverOpts::succinct()).await
239 }
240
241 pub async fn fulfill(
246 &self,
247 orders: &[Order],
248 ) -> Result<(Vec<BoundlessFulfillment>, Receipt, AssessorReceipt)> {
249 let orders_jobs = orders.iter().map(|order| async {
250 let request = order.request.clone();
251 let order_program = fetch_url(&request.imageUrl).await?;
252 let order_input: Vec<u8> = match request.input.inputType {
253 InputType::Inline => GuestEnv::decode(&request.input.data)?.stdin,
254 InputType::Url => {
255 GuestEnv::decode(
256 &fetch_url(
257 std::str::from_utf8(&request.input.data)
258 .context("input url is not utf8")?,
259 )
260 .await?,
261 )?
262 .stdin
263 }
264 _ => bail!("Unsupported input type"),
265 };
266
267 let selector = request.requirements.selector;
268 if !self.supported_selectors.is_supported(selector) {
269 bail!("Unsupported selector {}", request.requirements.selector);
270 };
271
272 let order_receipt = self
273 .prove(order_program.clone(), order_input.clone(), vec![], ProverOpts::succinct())
274 .await?;
275
276 let order_journal = order_receipt.journal.bytes.clone();
277 let order_image_id = compute_image_id(&order_program)?;
278 let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
279 let order_claim_digest = order_claim.digest();
280
281 let fill = Fulfillment {
282 request: order.request.clone(),
283 signature: order.signature.into(),
284 journal: order_journal.clone(),
285 };
286
287 Ok::<_, anyhow::Error>((order_receipt, order_claim, order_claim_digest, fill))
288 });
289
290 let results = futures::future::join_all(orders_jobs).await;
291 let mut receipts = Vec::new();
292 let mut claims = Vec::new();
293 let mut claim_digests = Vec::new();
294 let mut fills = Vec::new();
295
296 for (i, result) in results.into_iter().enumerate() {
297 if let Err(e) = result {
298 tracing::warn!("Failed to prove request 0x{:x}: {}", orders[i].request.id, e);
299 continue;
300 }
301 let (receipt, claim, claim_digest, fill) = result?;
302 receipts.push(receipt);
303 claims.push(claim);
304 claim_digests.push(claim_digest);
305 fills.push(fill);
306 }
307
308 let assessor_receipt = self.assessor(fills.clone(), receipts.clone()).await?;
309 let assessor_journal = assessor_receipt.journal.bytes.clone();
310 let assessor_image_id = compute_image_id(&self.assessor_program)?;
311 let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal.clone());
312 let assessor_receipt_journal: AssessorJournal =
313 AssessorJournal::abi_decode(&assessor_journal)?;
314
315 receipts.push(assessor_receipt);
316 claims.push(assessor_claim.clone());
317 claim_digests.push(assessor_claim.digest());
318
319 let root_receipt = self.finalize(claims.clone(), receipts.clone()).await?;
320
321 let verifier_parameters =
322 SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
323
324 let mut boundless_fills = Vec::new();
325
326 for i in 0..fills.len() {
327 let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
328 claims[i].clone(),
329 merkle_path(&claim_digests, i),
330 verifier_parameters.digest(),
331 );
332 let order = &orders[i];
333 let order_seal = if is_groth16_selector(order.request.requirements.selector) {
334 let receipt = self.compress(&receipts[i]).await?;
335 encode_seal(&receipt)?
336 } else {
337 order_inclusion_receipt.abi_encode_seal()?
338 };
339
340 let fulfillment = BoundlessFulfillment {
341 id: order.request.id,
342 requestDigest: order.request.eip712_signing_hash(&self.domain.alloy_struct()),
343 imageId: order.request.requirements.imageId,
344 journal: fills[i].journal.clone().into(),
345 seal: order_seal.into(),
346 };
347
348 boundless_fills.push(fulfillment);
349 }
350
351 let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
352 assessor_claim,
353 merkle_path(&claim_digests, claim_digests.len() - 1),
354 verifier_parameters.digest(),
355 );
356
357 let assessor_receipt = AssessorReceipt {
358 seal: assessor_inclusion_receipt.abi_encode_seal()?.into(),
359 prover: self.address,
360 selectors: assessor_receipt_journal.selectors,
361 callbacks: assessor_receipt_journal.callbacks,
362 };
363
364 Ok((boundless_fills, root_receipt, assessor_receipt))
365 }
366}
367
368async fn compress_with_bonsai(succinct_receipt: &Receipt) -> Result<Receipt> {
369 let client = BonsaiClient::from_env(risc0_zkvm::VERSION)?;
370 let encoded_receipt = bincode::serialize(succinct_receipt)?;
371 let receipt_id = client.upload_receipt(encoded_receipt).await?;
372 let snark_id = client.create_snark(receipt_id).await?;
373 loop {
374 let status = snark_id.status(&client).await?;
375 match status.status.as_ref() {
376 "RUNNING" => {
377 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
378 continue;
379 }
380 "SUCCEEDED" => {
381 let receipt_buf = client.download(&status.output.unwrap()).await?;
382 let snark_receipt: Receipt = bincode::deserialize(&receipt_buf)?;
383 return Ok(snark_receipt);
384 }
385 _ => {
386 let err_msg = status.error_msg.unwrap_or_default();
387 return Err(anyhow::anyhow!("snark proving failed: {err_msg}"));
388 }
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use alloy::{
397 primitives::{FixedBytes, Signature},
398 signers::local::PrivateKeySigner,
399 };
400 use boundless_market::contracts::{
401 eip712_domain, Input, Offer, Predicate, ProofRequest, RequestId, Requirements,
402 UNSPECIFIED_SELECTOR,
403 };
404 use boundless_market_test_utils::{ASSESSOR_GUEST_ELF, ECHO_ID, ECHO_PATH, SET_BUILDER_ELF};
405 use risc0_ethereum_contracts::selector::Selector;
406
407 async fn setup_proving_request_and_signature(
408 signer: &PrivateKeySigner,
409 selector: Option<Selector>,
410 ) -> (ProofRequest, Signature) {
411 let request = ProofRequest::new(
412 RequestId::new(signer.address(), 0),
413 Requirements::new(Digest::from(ECHO_ID), Predicate::prefix_match(vec![1]))
414 .with_selector(match selector {
415 Some(selector) => FixedBytes::from(selector as u32),
416 None => UNSPECIFIED_SELECTOR,
417 }),
418 format!("file://{ECHO_PATH}"),
419 Input::builder().write_slice(&[1, 2, 3, 4]).build_inline().unwrap(),
420 Offer::default(),
421 );
422
423 let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
424 (request, signature)
425 }
426
427 #[tokio::test]
428 #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
429 async fn test_fulfill_with_selector() {
430 let signer = PrivateKeySigner::random();
431 let (request, signature) =
432 setup_proving_request_and_signature(&signer, Some(Selector::Groth16V2_0)).await;
433
434 let domain = eip712_domain(Address::ZERO, 1);
435 let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
436 let prover = DefaultProver::new(
437 SET_BUILDER_ELF.to_vec(),
438 ASSESSOR_GUEST_ELF.to_vec(),
439 Address::ZERO,
440 domain,
441 )
442 .expect("failed to create prover");
443
444 let order = Order { request, request_digest, signature };
445 prover.fulfill(&[order.clone()]).await.unwrap();
446 }
447
448 #[tokio::test]
449 #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
450 async fn test_fulfill() {
451 let signer = PrivateKeySigner::random();
452 let (request, signature) = setup_proving_request_and_signature(&signer, None).await;
453
454 let domain = eip712_domain(Address::ZERO, 1);
455 let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
456 let prover = DefaultProver::new(
457 SET_BUILDER_ELF.to_vec(),
458 ASSESSOR_GUEST_ELF.to_vec(),
459 Address::ZERO,
460 domain,
461 )
462 .expect("failed to create prover");
463
464 let order = Order { request, request_digest, signature };
465 prover.fulfill(&[order.clone()]).await.unwrap();
466 }
467}