hyli_bonsai_runner/
lib.rs1use std::{
2 str::FromStr,
3 time::{Duration, SystemTime, UNIX_EPOCH},
4};
5
6use anyhow::{bail, Context, Result};
7use bonsai_sdk::non_blocking::Client;
8use borsh::BorshSerialize;
9use boundless_market::{
10 alloy::{
11 primitives::{utils::parse_ether, Uint},
12 signers::local::PrivateKeySigner,
13 transports::http::reqwest::Url,
14 },
15 client::ClientBuilder,
16 contracts::Offer,
17 deployments::NamedChain,
18 storage::{StandardUploader, StorageUploader, StorageUploaderConfig, StorageUploaderType},
19 Deployment, GuestEnvBuilder,
20};
21use risc0_zkvm::{compute_image_id, default_executor, sha::Digestible, Receipt};
22use tracing::info;
23
24#[allow(dead_code)]
25pub fn as_input_data<T: BorshSerialize>(data: &T) -> Result<Vec<u8>> {
26 let data = borsh::to_vec(&data)?;
27 let size = risc0_zkvm::serde::to_vec(&data.len())?;
28 let mut input_data = bytemuck::cast_slice(&size).to_vec();
29 input_data.extend(data);
30 Ok(input_data)
31}
32
33pub struct ProofResult {
34 pub receipt: Receipt,
35 pub cycles: Option<u64>,
36}
37
38fn is_dev_mode() -> bool {
39 std::env::var("RISC0_DEV_MODE")
40 .ok()
41 .map(|value| value.to_lowercase())
42 .filter(|value| value == "1" || value == "true" || value == "yes")
43 .is_some()
44}
45
46fn parse_url_env(key: &str) -> Result<Option<Url>> {
47 let value = match std::env::var(key) {
48 Ok(value) => value,
49 Err(std::env::VarError::NotPresent) => return Ok(None),
50 Err(err) => return Err(err.into()),
51 };
52
53 Ok(Some(
54 Url::parse(&value).with_context(|| format!("invalid URL in {key}"))?,
55 ))
56}
57
58async fn storage_uploader_from_env() -> Result<StandardUploader> {
59 if is_dev_mode() {
60 return Ok(StandardUploader::from_config(&StorageUploaderConfig::dev_mode()).await?);
61 }
62
63 if std::env::var("PINATA_JWT").is_ok() {
64 let mut config = StorageUploaderConfig::default();
65 config.storage_uploader = StorageUploaderType::Pinata;
66 config.pinata_jwt = std::env::var("PINATA_JWT").ok();
67 config.pinata_api_url = parse_url_env("PINATA_API_URL")?;
68 config.ipfs_gateway_url = parse_url_env("IPFS_GATEWAY_URL")?;
69 return Ok(StandardUploader::from_config(&config).await?);
70 }
71
72 anyhow::bail!(
73 "no storage uploader configured: set RISC0_DEV_MODE, PINATA_JWT, or storage env vars"
74 );
75}
76
77pub async fn run_boundless(elf: &[u8], input_data: Vec<u8>) -> Result<ProofResult> {
78 let chain_id = std::env::var("BOUNDLESS_CHAIN_ID").unwrap_or("11155111".to_string());
79 let offchain = std::env::var("BOUNDLESS_OFFCHAIN").unwrap_or_default() == "true";
80 let wallet_private_key = std::env::var("BOUNDLESS_WALLET_PRIVATE_KEY").unwrap_or_default();
81 let rpc_url = std::env::var("BOUNDLESS_RPC_URL").unwrap_or_default();
82
83 let min_price_per_mcycle =
84 std::env::var("BOUNDLESS_MIN_PRICE_PER_MCYCLE").unwrap_or_else(|_| "0.000001".to_string());
85 let max_price_per_mcycle =
86 std::env::var("BOUNDLESS_MAX_PRICE_PER_MCYCLE").unwrap_or_else(|_| "0.000005".to_string());
87 let timeout = std::env::var("BOUNDLESS_TIMEOUT").unwrap_or_else(|_| "120".to_string());
88 let lock_timeout = std::env::var("BOUNDLESS_LOCK_TIMEOUT").unwrap_or_else(|_| "60".to_string());
89 let ramp_up_period =
90 std::env::var("BOUNDLESS_RAMP_UP_PERIOD").unwrap_or_else(|_| "15".to_string());
91
92 let chain_id: u64 = chain_id.parse()?;
93 let min_price_per_mcycle = parse_ether(&min_price_per_mcycle)?;
94 let max_price_per_mcycle = parse_ether(&max_price_per_mcycle)?;
95 let timeout: u32 = timeout.parse()?;
96 let lock_timeout: u32 = lock_timeout.parse()?;
97 let ramp_up_period: u32 = ramp_up_period.parse()?;
98
99 let storage_uploader = storage_uploader_from_env().await?;
105
106 let image_url = storage_uploader.upload_program(elf).await?;
107 info!("Uploaded image to {}", image_url);
108
109 let wallet_private_key = PrivateKeySigner::from_str(&wallet_private_key)?;
110 let rpc_url = Url::parse(&rpc_url)?;
111
112 let mut deployment = Deployment::from_chain_id(chain_id);
113
114 if let Some(dep) = deployment.as_mut() {
115 if dep.market_chain_id.unwrap() == NamedChain::Base as u64 && chain_id == 84532 {
116 dep.market_chain_id = Some(NamedChain::BaseSepolia as u64);
117 }
118 }
119
120 let boundless_client = ClientBuilder::new()
122 .with_rpc_url(rpc_url)
123 .with_deployment(deployment)
124 .with_uploader(Some(storage_uploader))
125 .with_private_key(wallet_private_key)
126 .build()
127 .await
128 .context("failed to build boundless client")?;
129
130 let guest_env = risc0_zkvm::ExecutorEnv::builder()
132 .write_slice(&input_data)
133 .build()
134 .unwrap();
135
136 let session_info = default_executor().execute(guest_env, elf)?;
142 let cycles_count = session_info
143 .segments
144 .iter()
145 .map(|segment| 1 << segment.po2)
146 .sum::<u64>();
147 let mcycles_count = cycles_count.div_ceil(1_000_000);
148 let journal = session_info.journal;
149
150 info!(
151 "Dry run completed: {} mcycles, journal digest: {}",
152 mcycles_count,
153 journal.digest()
154 );
155
156 let address = boundless_client.signer.as_ref().unwrap().address();
157 let balance = boundless_client
158 .boundless_market
159 .balance_of(address)
160 .await?;
161 let max_price = max_price_per_mcycle * Uint::from(mcycles_count);
162 info!(address = %address, max_price = %max_price, "Wallet balance: {}", balance);
163 if balance < max_price {
164 let deposit = std::cmp::max(max_price, parse_ether("0.1")?);
165 info!(
166 "Wallet balance ({}) is low, depositing {} ETH",
167 balance, deposit
168 );
169 boundless_client.boundless_market.deposit(deposit).await?;
170 }
171
172 let env = GuestEnvBuilder::new().write_slice(&input_data).build_env();
196
197 let request = boundless_client
198 .new_request()
199 .with_program_url(image_url)?
200 .with_env(env)
201 .with_offer(
206 Offer::default()
207 .with_min_price_per_mcycle(min_price_per_mcycle, mcycles_count)
213 .with_max_price_per_mcycle(max_price_per_mcycle, mcycles_count)
215 .with_timeout(timeout)
220 .with_lock_timeout(lock_timeout)
221 .with_ramp_up_period(ramp_up_period),
222 );
223
224 let (request_id, expires_at) = if offchain {
226 boundless_client.submit_offchain(request).await?
227 } else {
228 boundless_client.submit_onchain(request).await?
229 };
230 tracing::info!("Request 0x{request_id:x} submitted");
231 tracing::info!("https://explorer.beboundless.xyz/orders/0x{request_id:x}");
232
233 let fullfillment = boundless_client
235 .wait_for_request_fulfillment(request_id, Duration::from_secs(3), expires_at)
236 .await?;
237 tracing::info!("Request 0x{request_id:x} fulfilled");
238
239 let journal = fullfillment
240 .data()?
241 .journal()
242 .ok_or_else(|| {
243 anyhow::anyhow!(
244 "Failed to get journal from fulfillment, this is likely a bug in the SDK"
245 )
246 })?
247 .clone();
248 let seal = fullfillment.seal;
249
250 std::fs::write("journal.bin", bincode::serialize(&journal)?)?;
252 std::fs::write("seal.bin", seal.clone())?;
253
254 let image_id = compute_image_id(elf)?;
255 let receipt = boundless_client
256 .set_verifier
257 .fetch_receipt(seal, image_id, journal.to_vec())
258 .await?;
259
260 let receipt = receipt.root.ok_or(anyhow::anyhow!(
261 "Failed to get root from receipt, this is likely a bug in the SDK"
262 ))?;
263
264 receipt.verify(image_id).context("Verify proof")?;
265
266 info!("Receipt verified successfully");
267
268 Ok(ProofResult {
269 receipt,
270 cycles: Some(cycles_count),
271 })
272}
273
274#[allow(dead_code)]
275pub async fn run_bonsai(elf: &[u8], input_data: Vec<u8>) -> Result<ProofResult> {
276 let client = Client::from_env(risc0_zkvm::VERSION)?;
277
278 let image_id = hex::encode(compute_image_id(elf)?);
280 client.upload_img(&image_id, elf.to_vec()).await?;
281
282 let input_id = client.upload_input(input_data).await?;
284
285 let assumptions: Vec<String> = vec![];
287
288 let execute_only = false;
290
291 let session = client
293 .create_session(image_id, input_id, assumptions, execute_only)
294 .await?;
295 loop {
296 let res = session.status(&client).await?;
297 if res.status == "RUNNING" {
298 info!(
299 "Current status: {} - state: {} - continue polling...",
300 res.status,
301 res.state.unwrap_or_default()
302 );
303 tokio::time::sleep(Duration::from_secs(1)).await;
304 continue;
305 }
306 if res.status == "SUCCEEDED" {
307 let receipt_url = res
309 .receipt_url
310 .expect("API error, missing receipt on completed session");
311
312 let receipt_buf = client.download(&receipt_url).await?;
313 let receipt: Receipt = bincode::deserialize(&receipt_buf)?;
314 return Ok(ProofResult {
315 receipt,
316 cycles: res.stats.map(|s| s.total_cycles),
317 });
318 } else {
319 bail!(
320 "Workflow exited: {} - | err: {}",
321 res.status,
322 res.error_msg.unwrap_or_default()
323 );
324 }
325 }
326}
327
328pub fn get_current_timestamp_secs() -> u64 {
329 SystemTime::now()
330 .duration_since(UNIX_EPOCH)
331 .expect("Time went backwards")
332 .as_secs()
333}