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};
36
37use boundless_market::{
38 contracts::{
39 AssessorJournal, AssessorReceipt, EIP712DomainSaltless,
40 Fulfillment as BoundlessFulfillment, RequestInputType,
41 },
42 input::GuestEnv,
43 order_stream_client::Order,
44 selector::{is_groth16_selector, SupportedSelectors},
45 storage::fetch_url,
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: &[Order],
219 ) -> Result<(Vec<BoundlessFulfillment>, Receipt, AssessorReceipt)> {
220 let orders_jobs = orders.iter().map(|order| async {
221 let request = order.request.clone();
222 let order_program = fetch_url(&request.imageUrl).await?;
223 let order_input: Vec<u8> = match request.input.inputType {
224 RequestInputType::Inline => GuestEnv::decode(&request.input.data)?.stdin,
225 RequestInputType::Url => {
226 GuestEnv::decode(
227 &fetch_url(
228 std::str::from_utf8(&request.input.data)
229 .context("input url is not utf8")?,
230 )
231 .await?,
232 )?
233 .stdin
234 }
235 _ => bail!("Unsupported input type"),
236 };
237
238 let selector = request.requirements.selector;
239 if !self.supported_selectors.is_supported(selector) {
240 bail!("Unsupported selector {}", request.requirements.selector);
241 };
242
243 let order_receipt = self
244 .prove(order_program.clone(), order_input.clone(), vec![], ProverOpts::succinct())
245 .await?;
246
247 let order_journal = order_receipt.journal.bytes.clone();
248 let order_image_id = compute_image_id(&order_program)?;
249 let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
250 let order_claim_digest = order_claim.digest();
251
252 let fill = Fulfillment {
253 request: order.request.clone(),
254 signature: order.signature.into(),
255 journal: order_journal.clone(),
256 };
257
258 Ok::<_, anyhow::Error>((order_receipt, order_claim, order_claim_digest, fill))
259 });
260
261 let results = futures::future::join_all(orders_jobs).await;
262 let mut receipts = Vec::new();
263 let mut claims = Vec::new();
264 let mut claim_digests = Vec::new();
265 let mut fills = Vec::new();
266
267 for (i, result) in results.into_iter().enumerate() {
268 if let Err(e) = result {
269 tracing::warn!("Failed to prove request 0x{:x}: {}", orders[i].request.id, e);
270 continue;
271 }
272 let (receipt, claim, claim_digest, fill) = result?;
273 receipts.push(receipt);
274 claims.push(claim);
275 claim_digests.push(claim_digest);
276 fills.push(fill);
277 }
278
279 let assessor_receipt = self.assessor(fills.clone(), receipts.clone()).await?;
280 let assessor_journal = assessor_receipt.journal.bytes.clone();
281 let assessor_image_id = compute_image_id(&self.assessor_program)?;
282 let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal.clone());
283 let assessor_receipt_journal: AssessorJournal =
284 AssessorJournal::abi_decode(&assessor_journal)?;
285
286 receipts.push(assessor_receipt);
287 claims.push(assessor_claim.clone());
288 claim_digests.push(assessor_claim.digest());
289
290 let root_receipt = self.finalize(claims.clone(), receipts.clone()).await?;
291
292 let verifier_parameters =
293 SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
294
295 let mut boundless_fills = Vec::new();
296
297 for i in 0..fills.len() {
298 let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
299 claims[i].clone(),
300 merkle_path(&claim_digests, i),
301 verifier_parameters.digest(),
302 );
303 let order = &orders[i];
304 let order_seal = if is_groth16_selector(order.request.requirements.selector) {
305 let receipt = self.compress(&receipts[i]).await?;
306 encode_seal(&receipt)?
307 } else {
308 order_inclusion_receipt.abi_encode_seal()?
309 };
310
311 let fulfillment = BoundlessFulfillment {
312 id: order.request.id,
313 requestDigest: order.request.eip712_signing_hash(&self.domain.alloy_struct()),
314 imageId: order.request.requirements.imageId,
315 journal: fills[i].journal.clone().into(),
316 seal: order_seal.into(),
317 };
318
319 boundless_fills.push(fulfillment);
320 }
321
322 let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
323 assessor_claim,
324 merkle_path(&claim_digests, claim_digests.len() - 1),
325 verifier_parameters.digest(),
326 );
327
328 let assessor_receipt = AssessorReceipt {
329 seal: assessor_inclusion_receipt.abi_encode_seal()?.into(),
330 prover: self.address,
331 selectors: assessor_receipt_journal.selectors,
332 callbacks: assessor_receipt_journal.callbacks,
333 };
334
335 Ok((boundless_fills, root_receipt, assessor_receipt))
336 }
337}
338
339async fn compress_with_bonsai(succinct_receipt: &Receipt) -> Result<Receipt> {
340 let client = BonsaiClient::from_env(risc0_zkvm::VERSION)?;
341 let encoded_receipt = bincode::serialize(succinct_receipt)?;
342 let receipt_id = client.upload_receipt(encoded_receipt).await?;
343 let snark_id = client.create_snark(receipt_id).await?;
344 loop {
345 let status = snark_id.status(&client).await?;
346 match status.status.as_ref() {
347 "RUNNING" => {
348 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
349 continue;
350 }
351 "SUCCEEDED" => {
352 let receipt_buf = client.download(&status.output.unwrap()).await?;
353 let snark_receipt: Receipt = bincode::deserialize(&receipt_buf)?;
354 return Ok(snark_receipt);
355 }
356 status_code => {
357 let err_msg = status.error_msg.unwrap_or_default();
358 return Err(anyhow::anyhow!(
359 "snark proving failed with status {status_code}: {err_msg}"
360 ));
361 }
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use alloy::{
370 primitives::{FixedBytes, Signature},
371 signers::local::PrivateKeySigner,
372 };
373 use boundless_market::contracts::{
374 eip712_domain, Offer, Predicate, ProofRequest, RequestId, RequestInput, Requirements,
375 UNSPECIFIED_SELECTOR,
376 };
377 use boundless_market_test_utils::{ASSESSOR_GUEST_ELF, ECHO_ID, ECHO_PATH, SET_BUILDER_ELF};
378 use risc0_ethereum_contracts::selector::Selector;
379
380 async fn setup_proving_request_and_signature(
381 signer: &PrivateKeySigner,
382 selector: Option<Selector>,
383 ) -> (ProofRequest, Signature) {
384 let request = ProofRequest::new(
385 RequestId::new(signer.address(), 0),
386 Requirements::new(Digest::from(ECHO_ID), Predicate::prefix_match(vec![1]))
387 .with_selector(match selector {
388 Some(selector) => FixedBytes::from(selector as u32),
389 None => UNSPECIFIED_SELECTOR,
390 }),
391 format!("file://{ECHO_PATH}"),
392 RequestInput::builder().write_slice(&[1, 2, 3, 4]).build_inline().unwrap(),
393 Offer::default(),
394 );
395
396 let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
397 (request, signature)
398 }
399
400 #[tokio::test]
401 #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
402 async fn test_fulfill_with_selector() {
403 let signer = PrivateKeySigner::random();
404 let (request, signature) =
405 setup_proving_request_and_signature(&signer, Some(Selector::Groth16V2_1)).await;
406
407 let domain = eip712_domain(Address::ZERO, 1);
408 let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
409 let prover = DefaultProver::new(
410 SET_BUILDER_ELF.to_vec(),
411 ASSESSOR_GUEST_ELF.to_vec(),
412 Address::ZERO,
413 domain,
414 )
415 .expect("failed to create prover");
416
417 let order = Order { request, request_digest, signature };
418 prover.fulfill(&[order.clone()]).await.unwrap();
419 }
420
421 #[tokio::test]
422 #[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
423 async fn test_fulfill() {
424 let signer = PrivateKeySigner::random();
425 let (request, signature) = setup_proving_request_and_signature(&signer, None).await;
426
427 let domain = eip712_domain(Address::ZERO, 1);
428 let request_digest = request.eip712_signing_hash(&domain.alloy_struct());
429 let prover = DefaultProver::new(
430 SET_BUILDER_ELF.to_vec(),
431 ASSESSOR_GUEST_ELF.to_vec(),
432 Address::ZERO,
433 domain,
434 )
435 .expect("failed to create prover");
436
437 let order = Order { request, request_digest, signature };
438 prover.fulfill(&[order.clone()]).await.unwrap();
439 }
440}