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 fill: 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: vec![fill],
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_elf: Vec<u8>,
139 set_builder_image_id: Digest,
140 assessor_elf: Vec<u8>,
141 address: Address,
142 domain: EIP712DomainSaltless,
143 supported_selectors: SupportedSelectors,
144}
145
146impl DefaultProver {
147 pub fn new(
149 set_builder_elf: Vec<u8>,
150 assessor_elf: Vec<u8>,
151 address: Address,
152 domain: EIP712DomainSaltless,
153 ) -> Result<Self> {
154 let set_builder_image_id = compute_image_id(&set_builder_elf)?;
155 let supported_selectors =
156 SupportedSelectors::default().with_set_builder_image_id(set_builder_image_id);
157 Ok(Self {
158 set_builder_elf,
159 set_builder_image_id,
160 assessor_elf,
161 address,
162 domain,
163 supported_selectors,
164 })
165 }
166
167 pub(crate) async fn prove(
170 &self,
171 elf: 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, &elf, &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(self.set_builder_elf.clone(), encoded_input, assumptions, ProverOpts::groth16())
219 .await
220 }
221
222 pub(crate) async fn assessor(
224 &self,
225 fills: Vec<Fulfillment>,
226 receipts: Vec<Receipt>,
227 ) -> Result<Receipt> {
228 let assessor_input =
229 AssessorInput { domain: self.domain.clone(), fills, prover_address: self.address };
230
231 let stdin = InputBuilder::new().write_frame(&assessor_input.encode()).stdin;
232
233 self.prove(self.assessor_elf.clone(), stdin, receipts, ProverOpts::succinct()).await
234 }
235
236 pub async fn fulfill(
242 &self,
243 order: Order,
244 ) -> Result<(BoundlessFulfillment, Receipt, AssessorReceipt)> {
245 let request = order.request.clone();
246 let order_elf = fetch_url(&request.imageUrl).await?;
247 let order_input: Vec<u8> = match request.input.inputType {
248 InputType::Inline => GuestEnv::decode(&request.input.data)?.stdin,
249 InputType::Url => {
250 GuestEnv::decode(
251 &fetch_url(
252 std::str::from_utf8(&request.input.data)
253 .context("input url is not utf8")?,
254 )
255 .await?,
256 )?
257 .stdin
258 }
259 _ => bail!("Unsupported input type"),
260 };
261
262 let selector = request.requirements.selector;
263 if !self.supported_selectors.is_supported(selector) {
264 bail!("Unsupported selector {}", request.requirements.selector);
265 };
266
267 let order_receipt = self
268 .prove(order_elf.clone(), order_input.clone(), vec![], ProverOpts::succinct())
269 .await?;
270
271 let order_journal = order_receipt.journal.bytes.clone();
272 let order_image_id = compute_image_id(&order_elf)?;
273
274 let fill = Fulfillment {
275 request: order.request.clone(),
276 signature: order.signature.into(),
277 journal: order_journal.clone(),
278 };
279
280 let assessor_receipt = self.assessor(vec![fill], vec![order_receipt.clone()]).await?;
281 let assessor_journal = assessor_receipt.journal.bytes.clone();
282 let assessor_image_id = compute_image_id(&self.assessor_elf)?;
283
284 let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
285 let order_claim_digest = order_claim.digest();
286 let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal.clone());
287 let assessor_claim_digest = assessor_claim.digest();
288 let assessor_receipt_journal: AssessorJournal =
289 AssessorJournal::abi_decode(&assessor_journal, true)?;
290 let root_receipt = self
291 .finalize(
292 vec![order_claim.clone(), assessor_claim.clone()],
293 vec![order_receipt.clone(), assessor_receipt],
294 )
295 .await?;
296
297 let order_path = merkle_path(&[order_claim_digest, assessor_claim_digest], 0);
298 let assessor_path = merkle_path(&[order_claim_digest, assessor_claim_digest], 1);
299
300 let verifier_parameters =
301 SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
302
303 let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
304 order_claim,
305 order_path,
306 verifier_parameters.digest(),
307 );
308 let order_seal = if is_groth16_selector(selector) {
309 let receipt = self.compress(&order_receipt).await?;
310 encode_seal(&receipt)?
311 } else {
312 order_inclusion_receipt.abi_encode_seal()?
313 };
314
315 let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
316 assessor_claim,
317 assessor_path,
318 verifier_parameters.digest(),
319 );
320
321 let fulfillment = BoundlessFulfillment {
322 id: request.id,
323 requestDigest: order.request.eip712_signing_hash(&self.domain.alloy_struct()),
324 imageId: request.requirements.imageId,
325 journal: order_journal.into(),
326 seal: order_seal.into(),
327 };
328
329 let assessor_receipt = AssessorReceipt {
330 seal: assessor_inclusion_receipt.abi_encode_seal()?.into(),
331 prover: self.address,
332 selectors: assessor_receipt_journal.selectors,
333 callbacks: assessor_receipt_journal.callbacks,
334 };
335
336 Ok((fulfillment, root_receipt, assessor_receipt))
337 }
338}
339
340async fn compress_with_bonsai(succinct_receipt: &Receipt) -> Result<Receipt> {
341 let client = BonsaiClient::from_env(risc0_zkvm::VERSION)?;
342 let encoded_receipt = bincode::serialize(succinct_receipt)?;
343 let receipt_id = client.upload_receipt(encoded_receipt).await?;
344 let snark_id = client.create_snark(receipt_id).await?;
345 loop {
346 let status = snark_id.status(&client).await?;
347 match status.status.as_ref() {
348 "RUNNING" => {
349 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
350 continue;
351 }
352 "SUCCEEDED" => {
353 let receipt_buf = client.download(&status.output.unwrap()).await?;
354 let snark_receipt: Receipt = bincode::deserialize(&receipt_buf)?;
355 return Ok(snark_receipt);
356 }
357 _ => {
358 let err_msg = status.error_msg.unwrap_or_default();
359 return Err(anyhow::anyhow!("snark proving failed: {err_msg}"));
360 }
361 }
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use alloy::{
369 primitives::{FixedBytes, PrimitiveSignature},
370 signers::local::PrivateKeySigner,
371 };
372 use boundless_market::contracts::{
373 eip712_domain, Input, Offer, Predicate, ProofRequest, RequestId, Requirements,
374 UNSPECIFIED_SELECTOR,
375 };
376 use guest_assessor::ASSESSOR_GUEST_ELF;
377 use guest_set_builder::SET_BUILDER_ELF;
378 use guest_util::{ECHO_ID, ECHO_PATH};
379 use risc0_ethereum_contracts::selector::Selector;
380
381 async fn setup_proving_request_and_signature(
382 signer: &PrivateKeySigner,
383 selector: Option<Selector>,
384 ) -> (ProofRequest, PrimitiveSignature) {
385 let request = ProofRequest::new(
386 RequestId::new(signer.address(), 0),
387 Requirements::new(Digest::from(ECHO_ID), Predicate::prefix_match(vec![1]))
388 .with_selector(match selector {
389 Some(selector) => FixedBytes::from(selector as u32),
390 None => UNSPECIFIED_SELECTOR,
391 }),
392 format!("file://{ECHO_PATH}"),
393 Input::builder().write_slice(&[1, 2, 3, 4]).build_inline().unwrap(),
394 Offer::default(),
395 );
396
397 let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
398 (request, signature)
399 }
400
401 #[tokio::test]
402 #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
403 async fn test_fulfill_with_selector() {
404 let signer = PrivateKeySigner::random();
405 let (request, signature) =
406 setup_proving_request_and_signature(&signer, Some(Selector::Groth16V2_0)).await;
407
408 let domain = eip712_domain(Address::ZERO, 1);
409 let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
410 let prover = DefaultProver::new(
411 SET_BUILDER_ELF.to_vec(),
412 ASSESSOR_GUEST_ELF.to_vec(),
413 Address::ZERO,
414 domain,
415 )
416 .expect("failed to create prover");
417
418 let order = Order { request, request_digest, signature };
419 prover.fulfill(order.clone()).await.unwrap();
420 }
421
422 #[tokio::test]
423 #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
424 async fn test_fulfill() {
425 let signer = PrivateKeySigner::random();
426 let (request, signature) = setup_proving_request_and_signature(&signer, None).await;
427
428 let domain = eip712_domain(Address::ZERO, 1);
429 let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
430 let prover = DefaultProver::new(
431 SET_BUILDER_ELF.to_vec(),
432 ASSESSOR_GUEST_ELF.to_vec(),
433 Address::ZERO,
434 domain,
435 )
436 .expect("failed to create prover");
437
438 let order = Order { request, request_digest, signature };
439 prover.fulfill(order.clone()).await.unwrap();
440 }
441}