1#![deny(missing_docs)]
18
19use alloy::{
20 primitives::{Address, Bytes},
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,
33 sha::{Digest, Digestible},
34 ExecutorEnv, ProverOpts, Receipt, ReceiptClaim,
35};
36
37use boundless_market::{
38 contracts::{
39 AssessorJournal, AssessorReceipt, EIP712DomainSaltless,
40 Fulfillment as BoundlessFulfillment, RequestInputType,
41 },
42 input::GuestEnv,
43 selector::{is_groth16_selector, SupportedSelectors},
44 storage::fetch_url,
45 ProofRequest,
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 struct DefaultProver {
109 set_builder_program: Vec<u8>,
110 set_builder_image_id: Digest,
111 assessor_program: Vec<u8>,
112 address: Address,
113 domain: EIP712DomainSaltless,
114 supported_selectors: SupportedSelectors,
115}
116
117impl DefaultProver {
118 pub fn new(
120 set_builder_program: Vec<u8>,
121 assessor_program: Vec<u8>,
122 address: Address,
123 domain: EIP712DomainSaltless,
124 ) -> Result<Self> {
125 let set_builder_image_id = compute_image_id(&set_builder_program)?;
126 let supported_selectors =
127 SupportedSelectors::default().with_set_builder_image_id(set_builder_image_id);
128 Ok(Self {
129 set_builder_program,
130 set_builder_image_id,
131 assessor_program,
132 address,
133 domain,
134 supported_selectors,
135 })
136 }
137
138 pub(crate) async fn prove(
141 &self,
142 program: Vec<u8>,
143 input: Vec<u8>,
144 assumptions: Vec<Receipt>,
145 opts: ProverOpts,
146 ) -> Result<Receipt> {
147 let receipt = tokio::task::spawn_blocking(move || {
148 let mut env = ExecutorEnv::builder();
149 env.write_slice(&input);
150 for assumption_receipt in assumptions.iter() {
151 env.add_assumption(assumption_receipt.clone());
152 }
153 let env = env.build()?;
154
155 default_prover().prove_with_opts(env, &program, &opts)
156 })
157 .await??
158 .receipt;
159 Ok(receipt)
160 }
161
162 pub(crate) async fn compress(&self, succinct_receipt: &Receipt) -> Result<Receipt> {
163 let prover = default_prover();
164 if prover.get_name() == "bonsai" {
165 return compress_with_bonsai(succinct_receipt).await;
166 }
167 if is_dev_mode() {
168 return Ok(succinct_receipt.clone());
169 }
170
171 let receipt = succinct_receipt.clone();
172 tokio::task::spawn_blocking(move || {
173 default_prover().compress(&ProverOpts::groth16(), &receipt)
174 })
175 .await?
176 }
177
178 pub(crate) async fn finalize(
180 &self,
181 claims: Vec<ReceiptClaim>,
182 assumptions: Vec<Receipt>,
183 ) -> Result<Receipt> {
184 let input = GuestState::initial(self.set_builder_image_id)
185 .into_input(claims, true)
186 .context("Failed to build set builder input")?;
187 let encoded_input = bytemuck::pod_collect_to_vec(&risc0_zkvm::serde::to_vec(&input)?);
188
189 self.prove(
190 self.set_builder_program.clone(),
191 encoded_input,
192 assumptions,
193 ProverOpts::groth16(),
194 )
195 .await
196 }
197
198 pub(crate) async fn assessor(
200 &self,
201 fills: Vec<Fulfillment>,
202 receipts: Vec<Receipt>,
203 ) -> Result<Receipt> {
204 let assessor_input =
205 AssessorInput { domain: self.domain.clone(), fills, prover_address: self.address };
206
207 let stdin = GuestEnv::builder().write_frame(&assessor_input.encode()).stdin;
208
209 self.prove(self.assessor_program.clone(), stdin, receipts, ProverOpts::succinct()).await
210 }
211
212 pub async fn fulfill(
217 &self,
218 orders: &[(ProofRequest, Bytes)],
219 ) -> Result<(Vec<BoundlessFulfillment>, Receipt, AssessorReceipt)> {
220 let orders_jobs = orders.iter().cloned().map(|(req, sig)| async move {
221 let order_program = fetch_url(&req.imageUrl).await?;
222 let order_input: Vec<u8> = match req.input.inputType {
223 RequestInputType::Inline => GuestEnv::decode(&req.input.data)?.stdin,
224 RequestInputType::Url => {
225 GuestEnv::decode(
226 &fetch_url(
227 std::str::from_utf8(&req.input.data)
228 .context("input url is not utf8")?,
229 )
230 .await?,
231 )?
232 .stdin
233 }
234 _ => bail!("Unsupported input type"),
235 };
236
237 let selector = req.requirements.selector;
238 if !self.supported_selectors.is_supported(selector) {
239 bail!("Unsupported selector {}", req.requirements.selector);
240 };
241
242 let order_receipt = self
243 .prove(order_program.clone(), order_input.clone(), vec![], ProverOpts::succinct())
244 .await?;
245
246 let order_journal = order_receipt.journal.bytes.clone();
247 let order_image_id = compute_image_id(&order_program)?;
248 let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
249 let order_claim_digest = order_claim.digest();
250
251 let fill = Fulfillment {
252 request: req.clone(),
253 signature: sig.into(),
254 journal: order_journal.clone(),
255 };
256
257 Ok::<_, anyhow::Error>((order_receipt, order_claim, order_claim_digest, fill))
258 });
259
260 let results = futures::future::join_all(orders_jobs).await;
261 let mut receipts = Vec::new();
262 let mut claims = Vec::new();
263 let mut claim_digests = Vec::new();
264 let mut fills = Vec::new();
265
266 for (i, result) in results.into_iter().enumerate() {
267 if let Err(e) = result {
268 tracing::warn!("Failed to prove request 0x{:x}: {}", orders[i].0.id, e);
269 continue;
270 }
271 let (receipt, claim, claim_digest, fill) = result?;
272 receipts.push(receipt);
273 claims.push(claim);
274 claim_digests.push(claim_digest);
275 fills.push(fill);
276 }
277
278 let assessor_receipt = self.assessor(fills.clone(), receipts.clone()).await?;
279 let assessor_journal = assessor_receipt.journal.bytes.clone();
280 let assessor_image_id = compute_image_id(&self.assessor_program)?;
281 let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal.clone());
282 let assessor_receipt_journal: AssessorJournal =
283 AssessorJournal::abi_decode(&assessor_journal)?;
284
285 receipts.push(assessor_receipt);
286 claims.push(assessor_claim.clone());
287 claim_digests.push(assessor_claim.digest());
288
289 let root_receipt = self.finalize(claims.clone(), receipts.clone()).await?;
290
291 let verifier_parameters =
292 SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
293
294 let mut boundless_fills = Vec::new();
295
296 for i in 0..fills.len() {
297 let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
298 claims[i].clone(),
299 merkle_path(&claim_digests, i),
300 verifier_parameters.digest(),
301 );
302 let (req, _sig) = &orders[i];
303 let order_seal = if is_groth16_selector(req.requirements.selector) {
304 let receipt = self.compress(&receipts[i]).await?;
305 encode_seal(&receipt)?
306 } else {
307 order_inclusion_receipt.abi_encode_seal()?
308 };
309
310 let fulfillment = BoundlessFulfillment {
311 id: req.id,
312 requestDigest: req.eip712_signing_hash(&self.domain.alloy_struct()),
313 imageId: req.requirements.imageId,
314 journal: fills[i].journal.clone().into(),
315 seal: order_seal.into(),
316 };
317
318 boundless_fills.push(fulfillment);
319 }
320
321 let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
322 assessor_claim,
323 merkle_path(&claim_digests, claim_digests.len() - 1),
324 verifier_parameters.digest(),
325 );
326
327 let assessor_receipt = AssessorReceipt {
328 seal: assessor_inclusion_receipt.abi_encode_seal()?.into(),
329 prover: self.address,
330 selectors: assessor_receipt_journal.selectors,
331 callbacks: assessor_receipt_journal.callbacks,
332 };
333
334 Ok((boundless_fills, root_receipt, assessor_receipt))
335 }
336}
337
338async fn compress_with_bonsai(succinct_receipt: &Receipt) -> Result<Receipt> {
339 let client = BonsaiClient::from_env(risc0_zkvm::VERSION)?;
340 let encoded_receipt = bincode::serialize(succinct_receipt)?;
341 let receipt_id = client.upload_receipt(encoded_receipt).await?;
342 let snark_id = client.create_snark(receipt_id).await?;
343 loop {
344 let status = snark_id.status(&client).await?;
345 match status.status.as_ref() {
346 "RUNNING" => {
347 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
348 continue;
349 }
350 "SUCCEEDED" => {
351 let receipt_buf = client.download(&status.output.unwrap()).await?;
352 let snark_receipt: Receipt = bincode::deserialize(&receipt_buf)?;
353 return Ok(snark_receipt);
354 }
355 status_code => {
356 let err_msg = status.error_msg.unwrap_or_default();
357 return Err(anyhow::anyhow!(
358 "snark proving failed with status {status_code}: {err_msg}"
359 ));
360 }
361 }
362 }
363}
364
365fn is_dev_mode() -> bool {
367 std::env::var("RISC0_DEV_MODE")
368 .ok()
369 .map(|x| x.to_lowercase())
370 .filter(|x| x == "1" || x == "true" || x == "yes")
371 .is_some()
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use alloy::{
378 primitives::{FixedBytes, Signature},
379 signers::local::PrivateKeySigner,
380 };
381 use boundless_market::contracts::{
382 eip712_domain, Offer, Predicate, ProofRequest, RequestId, RequestInput, Requirements,
383 UNSPECIFIED_SELECTOR,
384 };
385 use boundless_market_test_utils::{ASSESSOR_GUEST_ELF, ECHO_ID, ECHO_PATH, SET_BUILDER_ELF};
386 use risc0_ethereum_contracts::selector::Selector;
387
388 async fn setup_proving_request_and_signature(
389 signer: &PrivateKeySigner,
390 selector: Option<Selector>,
391 ) -> (ProofRequest, Signature) {
392 let request = ProofRequest::new(
393 RequestId::new(signer.address(), 0),
394 Requirements::new(Digest::from(ECHO_ID), Predicate::prefix_match(vec![1]))
395 .with_selector(match selector {
396 Some(selector) => FixedBytes::from(selector as u32),
397 None => UNSPECIFIED_SELECTOR,
398 }),
399 format!("file://{ECHO_PATH}"),
400 RequestInput::builder().write_slice(&[1, 2, 3, 4]).build_inline().unwrap(),
401 Offer::default(),
402 );
403
404 let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
405 (request, signature)
406 }
407
408 #[tokio::test]
409 #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
410 async fn test_fulfill_with_selector() {
411 let signer = PrivateKeySigner::random();
412 let (request, signature) =
413 setup_proving_request_and_signature(&signer, Some(Selector::groth16_latest())).await;
414
415 let domain = eip712_domain(Address::ZERO, 1);
416 let prover = DefaultProver::new(
417 SET_BUILDER_ELF.to_vec(),
418 ASSESSOR_GUEST_ELF.to_vec(),
419 Address::ZERO,
420 domain,
421 )
422 .expect("failed to create prover");
423
424 prover.fulfill(&[(request, signature.as_bytes().into())]).await.unwrap();
425 }
426
427 #[tokio::test]
428 #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
429 async fn test_fulfill() {
430 let signer = PrivateKeySigner::random();
431 let (request, signature) = setup_proving_request_and_signature(&signer, None).await;
432
433 let domain = eip712_domain(Address::ZERO, 1);
434 let prover = DefaultProver::new(
435 SET_BUILDER_ELF.to_vec(),
436 ASSESSOR_GUEST_ELF.to_vec(),
437 Address::ZERO,
438 domain,
439 )
440 .expect("failed to create prover");
441
442 prover.fulfill(&[(request, signature.as_bytes().into())]).await.unwrap();
443 }
444}