#![deny(missing_docs)]
use alloy::{
primitives::{Address, Bytes},
sol_types::{SolStruct, SolValue},
};
use anyhow::{bail, Context, Result};
use bonsai_sdk::non_blocking::Client as BonsaiClient;
use boundless_assessor::{AssessorInput, Fulfillment};
use chrono::{DateTime, Local};
use risc0_aggregation::{
merkle_path, GuestState, SetInclusionReceipt, SetInclusionReceiptVerifierParameters,
};
use risc0_ethereum_contracts::encode_seal;
use risc0_zkvm::{
compute_image_id, default_prover,
sha::{Digest, Digestible},
ExecutorEnv, ProverOpts, Receipt, ReceiptClaim,
};
use boundless_market::{
contracts::{
AssessorJournal, AssessorReceipt, EIP712DomainSaltless,
Fulfillment as BoundlessFulfillment, RequestInputType,
},
input::GuestEnv,
selector::{is_groth16_selector, SupportedSelectors},
storage::fetch_url,
ProofRequest,
};
alloy::sol!(
#[sol(all_derives)]
struct OrderFulfilled {
bytes32 root;
bytes seal;
BoundlessFulfillment[] fills;
AssessorReceipt assessorReceipt;
}
);
impl OrderFulfilled {
pub fn new(
fills: Vec<BoundlessFulfillment>,
root_receipt: Receipt,
assessor_receipt: AssessorReceipt,
) -> Result<Self> {
let state = GuestState::decode(&root_receipt.journal.bytes)?;
let root = state.mmr.finalized_root().context("failed to get finalized root")?;
let root_seal = encode_seal(&root_receipt)?;
Ok(OrderFulfilled {
root: <[u8; 32]>::from(root).into(),
seal: root_seal.into(),
fills,
assessorReceipt: assessor_receipt,
})
}
}
pub fn convert_timestamp(timestamp: u64) -> DateTime<Local> {
let t = DateTime::from_timestamp(timestamp as i64, 0).expect("invalid timestamp");
t.with_timezone(&Local)
}
pub struct DefaultProver {
set_builder_program: Vec<u8>,
set_builder_image_id: Digest,
assessor_program: Vec<u8>,
address: Address,
domain: EIP712DomainSaltless,
supported_selectors: SupportedSelectors,
}
impl DefaultProver {
pub fn new(
set_builder_program: Vec<u8>,
assessor_program: Vec<u8>,
address: Address,
domain: EIP712DomainSaltless,
) -> Result<Self> {
let set_builder_image_id = compute_image_id(&set_builder_program)?;
let supported_selectors =
SupportedSelectors::default().with_set_builder_image_id(set_builder_image_id);
Ok(Self {
set_builder_program,
set_builder_image_id,
assessor_program,
address,
domain,
supported_selectors,
})
}
pub(crate) async fn prove(
&self,
program: Vec<u8>,
input: Vec<u8>,
assumptions: Vec<Receipt>,
opts: ProverOpts,
) -> Result<Receipt> {
let receipt = tokio::task::spawn_blocking(move || {
let mut env = ExecutorEnv::builder();
env.write_slice(&input);
for assumption_receipt in assumptions.iter() {
env.add_assumption(assumption_receipt.clone());
}
let env = env.build()?;
default_prover().prove_with_opts(env, &program, &opts)
})
.await??
.receipt;
Ok(receipt)
}
pub(crate) async fn compress(&self, succinct_receipt: &Receipt) -> Result<Receipt> {
let prover = default_prover();
if prover.get_name() == "bonsai" {
return compress_with_bonsai(succinct_receipt).await;
}
if is_dev_mode() {
return Ok(succinct_receipt.clone());
}
let receipt = succinct_receipt.clone();
tokio::task::spawn_blocking(move || {
default_prover().compress(&ProverOpts::groth16(), &receipt)
})
.await?
}
pub(crate) async fn finalize(
&self,
claims: Vec<ReceiptClaim>,
assumptions: Vec<Receipt>,
) -> Result<Receipt> {
let input = GuestState::initial(self.set_builder_image_id)
.into_input(claims, true)
.context("Failed to build set builder input")?;
let encoded_input = bytemuck::pod_collect_to_vec(&risc0_zkvm::serde::to_vec(&input)?);
self.prove(
self.set_builder_program.clone(),
encoded_input,
assumptions,
ProverOpts::groth16(),
)
.await
}
pub(crate) async fn assessor(
&self,
fills: Vec<Fulfillment>,
receipts: Vec<Receipt>,
) -> Result<Receipt> {
let assessor_input =
AssessorInput { domain: self.domain.clone(), fills, prover_address: self.address };
let stdin = GuestEnv::builder().write_frame(&assessor_input.encode()).stdin;
self.prove(self.assessor_program.clone(), stdin, receipts, ProverOpts::succinct()).await
}
pub async fn fulfill(
&self,
orders: &[(ProofRequest, Bytes)],
) -> Result<(Vec<BoundlessFulfillment>, Receipt, AssessorReceipt)> {
let orders_jobs = orders.iter().cloned().map(|(req, sig)| async move {
let order_program = fetch_url(&req.imageUrl).await?;
let order_input: Vec<u8> = match req.input.inputType {
RequestInputType::Inline => GuestEnv::decode(&req.input.data)?.stdin,
RequestInputType::Url => {
GuestEnv::decode(
&fetch_url(
std::str::from_utf8(&req.input.data)
.context("input url is not utf8")?,
)
.await?,
)?
.stdin
}
_ => bail!("Unsupported input type"),
};
let selector = req.requirements.selector;
if !self.supported_selectors.is_supported(selector) {
bail!("Unsupported selector {}", req.requirements.selector);
};
let order_receipt = self
.prove(order_program.clone(), order_input.clone(), vec![], ProverOpts::succinct())
.await?;
let order_journal = order_receipt.journal.bytes.clone();
let order_image_id = compute_image_id(&order_program)?;
let order_claim = ReceiptClaim::ok(order_image_id, order_journal.clone());
let order_claim_digest = order_claim.digest();
let fill = Fulfillment {
request: req.clone(),
signature: sig.into(),
journal: order_journal.clone(),
};
Ok::<_, anyhow::Error>((order_receipt, order_claim, order_claim_digest, fill))
});
let results = futures::future::join_all(orders_jobs).await;
let mut receipts = Vec::new();
let mut claims = Vec::new();
let mut claim_digests = Vec::new();
let mut fills = Vec::new();
for (i, result) in results.into_iter().enumerate() {
if let Err(e) = result {
tracing::warn!("Failed to prove request 0x{:x}: {}", orders[i].0.id, e);
continue;
}
let (receipt, claim, claim_digest, fill) = result?;
receipts.push(receipt);
claims.push(claim);
claim_digests.push(claim_digest);
fills.push(fill);
}
let assessor_receipt = self.assessor(fills.clone(), receipts.clone()).await?;
let assessor_journal = assessor_receipt.journal.bytes.clone();
let assessor_image_id = compute_image_id(&self.assessor_program)?;
let assessor_claim = ReceiptClaim::ok(assessor_image_id, assessor_journal.clone());
let assessor_receipt_journal: AssessorJournal =
AssessorJournal::abi_decode(&assessor_journal)?;
receipts.push(assessor_receipt);
claims.push(assessor_claim.clone());
claim_digests.push(assessor_claim.digest());
let root_receipt = self.finalize(claims.clone(), receipts.clone()).await?;
let verifier_parameters =
SetInclusionReceiptVerifierParameters { image_id: self.set_builder_image_id };
let mut boundless_fills = Vec::new();
for i in 0..fills.len() {
let order_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
claims[i].clone(),
merkle_path(&claim_digests, i),
verifier_parameters.digest(),
);
let (req, _sig) = &orders[i];
let order_seal = if is_groth16_selector(req.requirements.selector) {
let receipt = self.compress(&receipts[i]).await?;
encode_seal(&receipt)?
} else {
order_inclusion_receipt.abi_encode_seal()?
};
let fulfillment = BoundlessFulfillment {
id: req.id,
requestDigest: req.eip712_signing_hash(&self.domain.alloy_struct()),
imageId: req.requirements.imageId,
journal: fills[i].journal.clone().into(),
seal: order_seal.into(),
};
boundless_fills.push(fulfillment);
}
let assessor_inclusion_receipt = SetInclusionReceipt::from_path_with_verifier_params(
assessor_claim,
merkle_path(&claim_digests, claim_digests.len() - 1),
verifier_parameters.digest(),
);
let assessor_receipt = AssessorReceipt {
seal: assessor_inclusion_receipt.abi_encode_seal()?.into(),
prover: self.address,
selectors: assessor_receipt_journal.selectors,
callbacks: assessor_receipt_journal.callbacks,
};
Ok((boundless_fills, root_receipt, assessor_receipt))
}
}
async fn compress_with_bonsai(succinct_receipt: &Receipt) -> Result<Receipt> {
let client = BonsaiClient::from_env(risc0_zkvm::VERSION)?;
let encoded_receipt = bincode::serialize(succinct_receipt)?;
let receipt_id = client.upload_receipt(encoded_receipt).await?;
let snark_id = client.create_snark(receipt_id).await?;
loop {
let status = snark_id.status(&client).await?;
match status.status.as_ref() {
"RUNNING" => {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
continue;
}
"SUCCEEDED" => {
let receipt_buf = client.download(&status.output.unwrap()).await?;
let snark_receipt: Receipt = bincode::deserialize(&receipt_buf)?;
return Ok(snark_receipt);
}
status_code => {
let err_msg = status.error_msg.unwrap_or_default();
return Err(anyhow::anyhow!(
"snark proving failed with status {status_code}: {err_msg}"
));
}
}
}
}
fn is_dev_mode() -> bool {
std::env::var("RISC0_DEV_MODE")
.ok()
.map(|x| x.to_lowercase())
.filter(|x| x == "1" || x == "true" || x == "yes")
.is_some()
}
#[cfg(test)]
mod tests {
use super::*;
use alloy::{
primitives::{FixedBytes, Signature},
signers::local::PrivateKeySigner,
};
use boundless_market::contracts::{
eip712_domain, Offer, Predicate, ProofRequest, RequestId, RequestInput, Requirements,
UNSPECIFIED_SELECTOR,
};
use boundless_market_test_utils::{ASSESSOR_GUEST_ELF, ECHO_ID, ECHO_PATH, SET_BUILDER_ELF};
use risc0_ethereum_contracts::selector::Selector;
async fn setup_proving_request_and_signature(
signer: &PrivateKeySigner,
selector: Option<Selector>,
) -> (ProofRequest, Signature) {
let request = ProofRequest::new(
RequestId::new(signer.address(), 0),
Requirements::new(Digest::from(ECHO_ID), Predicate::prefix_match(vec![1]))
.with_selector(match selector {
Some(selector) => FixedBytes::from(selector as u32),
None => UNSPECIFIED_SELECTOR,
}),
format!("file://{ECHO_PATH}"),
RequestInput::builder().write_slice(&[1, 2, 3, 4]).build_inline().unwrap(),
Offer::default(),
);
let signature = request.sign_request(signer, Address::ZERO, 1).await.unwrap();
(request, signature)
}
#[tokio::test]
#[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
async fn test_fulfill_with_selector() {
let signer = PrivateKeySigner::random();
let (request, signature) =
setup_proving_request_and_signature(&signer, Some(Selector::groth16_latest())).await;
let domain = eip712_domain(Address::ZERO, 1);
let prover = DefaultProver::new(
SET_BUILDER_ELF.to_vec(),
ASSESSOR_GUEST_ELF.to_vec(),
Address::ZERO,
domain,
)
.expect("failed to create prover");
prover.fulfill(&[(request, signature.as_bytes().into())]).await.unwrap();
}
#[tokio::test]
#[ignore = "runs a proof; slow without RISC0_DEV_MODE=1"]
async fn test_fulfill() {
let signer = PrivateKeySigner::random();
let (request, signature) = setup_proving_request_and_signature(&signer, None).await;
let domain = eip712_domain(Address::ZERO, 1);
let prover = DefaultProver::new(
SET_BUILDER_ELF.to_vec(),
ASSESSOR_GUEST_ELF.to_vec(),
Address::ZERO,
domain,
)
.expect("failed to create prover");
prover.fulfill(&[(request, signature.as_bytes().into())]).await.unwrap();
}
}