1use std::{
16 borrow::Cow,
17 fmt,
18 fmt::{Debug, Display},
19 future::Future,
20};
21
22use alloy::{
23 network::Ethereum,
24 providers::{DynProvider, Provider},
25};
26use derive_builder::Builder;
27use risc0_zkvm::{Digest, Journal};
28use url::Url;
29
30use crate::{
31 contracts::{ProofRequest, RequestId, RequestInput},
32 input::GuestEnv,
33 prover_utils::requestor_pricing::requestor_order_preflight,
34 selector::SelectorExt,
35 storage::{StandardDownloader, StorageDownloader},
36 util::{now_timestamp, NotProvided},
37 StandardUploader,
38};
39mod preflight_layer;
40mod storage_layer;
41
42pub use preflight_layer::PreflightLayer;
43pub use storage_layer::{StorageLayer, StorageLayerConfig, StorageLayerConfigBuilder};
44mod requirements_layer;
45pub use requirements_layer::{RequirementParams, RequirementsLayer};
46mod request_id_layer;
47pub use request_id_layer::{
48 RequestIdLayer, RequestIdLayerConfig, RequestIdLayerConfigBuilder, RequestIdLayerMode,
49};
50mod offer_layer;
51pub use offer_layer::{
52 OfferLayer, OfferLayerConfig, OfferLayerConfigBuilder, OfferParams, OfferParamsBuilder,
53};
54mod finalizer;
55pub use finalizer::{Finalizer, FinalizerConfig, FinalizerConfigBuilder};
56
57pub trait RequestBuilder<Params> {
61 type Error;
63
64 fn params(&self) -> Params
69 where
70 Params: Default,
71 {
72 Default::default()
73 }
74
75 fn build(
77 &self,
78 params: impl Into<Params>,
79 ) -> impl Future<Output = Result<ProofRequest, Self::Error>>;
80}
81
82impl<L, Params> RequestBuilder<Params> for L
87where
88 L: Layer<Params, Output = ProofRequest>,
89{
90 type Error = L::Error;
91
92 async fn build(&self, params: impl Into<Params>) -> Result<ProofRequest, Self::Error> {
93 self.process(params.into()).await
94 }
95}
96
97pub trait Layer<Input> {
103 type Output;
105
106 type Error;
108
109 fn process(&self, input: Input) -> impl Future<Output = Result<Self::Output, Self::Error>>;
111}
112
113pub trait Adapt<L> {
122 type Output;
124
125 type Error;
127
128 fn process_with(self, layer: &L) -> impl Future<Output = Result<Self::Output, Self::Error>>;
130}
131
132impl<L, I> Adapt<L> for I
133where
134 L: Layer<I>,
135{
136 type Output = L::Output;
137 type Error = L::Error;
138
139 async fn process_with(self, layer: &L) -> Result<Self::Output, Self::Error> {
140 layer.process(self).await
141 }
142}
143
144impl<A, B, Input> Layer<Input> for (A, B)
146where
147 Input: Adapt<A>,
148 <Input as Adapt<A>>::Output: Adapt<B>,
149 <Input as Adapt<A>>::Error: Into<<<Input as Adapt<A>>::Output as Adapt<B>>::Error>,
150{
151 type Output = <<Input as Adapt<A>>::Output as Adapt<B>>::Output;
152 type Error = <<Input as Adapt<A>>::Output as Adapt<B>>::Error;
153
154 async fn process(&self, input: Input) -> Result<Self::Output, Self::Error> {
155 input.process_with(&self.0).await.map_err(Into::into)?.process_with(&self.1).await
156 }
157}
158
159#[derive(Clone, Builder)]
172#[non_exhaustive]
173pub struct StandardRequestBuilder<P = DynProvider, U = StandardUploader, D = StandardDownloader> {
174 #[builder(setter(into), default)]
176 pub storage_layer: StorageLayer<U>,
177
178 #[builder(setter(into), default)]
180 pub preflight_layer: PreflightLayer<D>,
181
182 #[builder(setter(into), default)]
184 pub requirements_layer: RequirementsLayer,
185
186 #[builder(setter(into))]
188 pub request_id_layer: RequestIdLayer<P>,
189
190 #[builder(setter(into))]
192 pub offer_layer: OfferLayer<P>,
193
194 #[builder(setter(into), default)]
196 pub finalizer: Finalizer,
197
198 #[builder(setter(into, strip_option), default)]
204 pub skip_preflight: Option<bool>,
205}
206
207impl StandardRequestBuilder<NotProvided, NotProvided, NotProvided> {
208 pub fn builder<P: Clone, S: Clone, D: Clone>() -> StandardRequestBuilderBuilder<P, S, D> {
218 Default::default()
219 }
220}
221
222impl<P, S, D> StandardRequestBuilder<P, S, D>
223where
224 P: Provider<Ethereum> + 'static + Clone,
225 D: StorageDownloader,
226{
227 fn should_skip_preflight(&self) -> bool {
228 self.skip_preflight.unwrap_or_else(|| std::env::var("BOUNDLESS_IGNORE_PREFLIGHT").is_ok())
229 }
230
231 async fn run_pricing_check(&self, request: &ProofRequest) {
232 if self.should_skip_preflight() {
233 return;
234 }
235
236 let market_address = *self.request_id_layer.boundless_market.instance().address();
237 let chain_id = match self.request_id_layer.boundless_market.get_chain_id().await {
238 Ok(id) => id,
239 Err(e) => {
240 tracing::error!("Failed to get chain ID for pricing check: {:#}", e);
241 return;
242 }
243 };
244 let provider = std::sync::Arc::new(self.offer_layer.provider.clone());
245 let price_provider = self.offer_layer.price_provider.clone();
246
247 if let Err(e) = requestor_order_preflight(
248 request.clone(),
249 self.preflight_layer.executor_cloned(),
250 provider,
251 market_address,
252 chain_id,
253 price_provider,
254 )
255 .await
256 {
257 tracing::error!("Pricing check failed: {:#}", e);
258 }
259 }
260
261 async fn build_from_params(
262 &self,
263 params: RequestParams,
264 ) -> Result<ProofRequest, anyhow::Error> {
265 let params = params
266 .process_with(&self.preflight_layer)
267 .await?
268 .process_with(&self.requirements_layer)
269 .await?
270 .process_with(&self.request_id_layer)
271 .await?
272 .process_with(&self.offer_layer)
273 .await?;
274
275 let request = params.process_with(&self.finalizer).await?;
276 self.run_pricing_check(&request).await;
277 Ok(request)
278 }
279}
280
281impl<P> StandardRequestBuilder<P, NotProvided>
282where
283 P: Provider<Ethereum> + 'static + Clone,
284{
285 pub async fn build(
292 &self,
293 input: impl Into<RequestParams>,
294 ) -> Result<ProofRequest, anyhow::Error> {
295 let params = input.into().process_with(&self.storage_layer).await?;
296 self.build_from_params(params).await
297 }
298}
299
300impl<P, S, D> Layer<RequestParams> for StandardRequestBuilder<P, S, D>
301where
302 P: Provider<Ethereum> + 'static + Clone,
303 D: StorageDownloader,
304 RequestParams: Adapt<StorageLayer<S>, Output = RequestParams, Error = anyhow::Error>,
305 RequestParams: Adapt<PreflightLayer<D>, Output = RequestParams, Error = anyhow::Error>,
306{
307 type Output = ProofRequest;
308 type Error = anyhow::Error;
309
310 async fn process(&self, input: RequestParams) -> Result<ProofRequest, Self::Error> {
311 let params = input.process_with(&self.storage_layer).await?;
312 self.build_from_params(params).await
313 }
314}
315
316#[non_exhaustive]
328#[derive(Clone, Default)]
329pub struct RequestParams {
330 pub program: Option<Cow<'static, [u8]>>,
332
333 pub env: Option<GuestEnv>,
336
337 pub program_url: Option<Url>,
339
340 pub request_input: Option<RequestInput>,
343
344 pub cycles: Option<u64>,
346
347 pub image_id: Option<Digest>,
349
350 pub journal: Option<Journal>,
352
353 pub request_id: Option<RequestId>,
355
356 pub offer: OfferParams,
358
359 pub requirements: RequirementParams,
361}
362
363impl RequestParams {
364 pub fn new() -> Self {
369 Self::default()
370 }
371
372 pub fn require_program(&self) -> Result<&[u8], MissingFieldError> {
377 self.program
378 .as_deref()
379 .ok_or(MissingFieldError::with_hint("program", "can be set using .with_program(...)"))
380 }
381
382 pub fn with_program(self, value: impl Into<Cow<'static, [u8]>>) -> Self {
384 Self { program: Some(value.into()), ..self }
385 }
386
387 pub fn require_env(&self) -> Result<&GuestEnv, MissingFieldError> {
391 self.env.as_ref().ok_or(MissingFieldError::with_hint(
392 "env",
393 "can be set using .with_env(...) or .with_stdin",
394 ))
395 }
396
397 pub fn with_env(self, value: impl Into<GuestEnv>) -> Self {
415 Self { env: Some(value.into()), ..self }
416 }
417
418 pub fn with_stdin(self, value: impl Into<Vec<u8>>) -> Self {
436 Self { env: Some(GuestEnv::from_stdin(value)), ..self }
437 }
438
439 pub fn require_program_url(&self) -> Result<&Url, MissingFieldError> {
443 self.program_url.as_ref().ok_or(MissingFieldError::with_hint(
444 "program_url",
445 "can be set using .with_program_url(...)",
446 ))
447 }
448
449 pub fn with_program_url<T: TryInto<Url>>(self, value: T) -> Result<Self, T::Error> {
460 Ok(Self { program_url: Some(value.try_into()?), ..self })
461 }
462
463 pub fn require_request_input(&self) -> Result<&RequestInput, MissingFieldError> {
467 self.request_input.as_ref().ok_or(MissingFieldError::with_hint(
468 "request_input",
469 "can be set using .with_request_input(...)",
470 ))
471 }
472
473 pub fn with_request_input(self, value: impl Into<RequestInput>) -> Self {
480 Self { request_input: Some(value.into()), ..self }
481 }
482
483 pub fn with_input_url<T: TryInto<Url>>(self, value: T) -> Result<Self, T::Error> {
496 Ok(Self { request_input: Some(RequestInput::url(value.try_into()?)), ..self })
497 }
498
499 pub fn require_cycles(&self) -> Result<u64, MissingFieldError> {
503 self.cycles
504 .ok_or(MissingFieldError::with_hint("cycles", "can be set using .with_cycles(...)"))
505 }
506
507 pub fn with_cycles(self, value: u64) -> Self {
511 Self { cycles: Some(value), ..self }
512 }
513
514 pub fn require_journal(&self) -> Result<&Journal, MissingFieldError> {
518 self.journal
519 .as_ref()
520 .ok_or(MissingFieldError::with_hint("journal", "can be set using .with_journal(...)"))
521 }
522
523 pub fn with_journal(self, value: impl Into<Journal>) -> Self {
528 Self { journal: Some(value.into()), ..self }
529 }
530
531 pub fn require_image_id(&self) -> Result<Digest, MissingFieldError> {
535 self.image_id.ok_or(MissingFieldError::with_hint(
536 "image_id",
537 "can be set using .with_image_id(...), and is calculated from the program",
538 ))
539 }
540
541 pub fn with_image_id(self, value: impl Into<Digest>) -> Self {
546 Self { image_id: Some(value.into()), ..self }
547 }
548
549 pub fn require_request_id(&self) -> Result<&RequestId, MissingFieldError> {
554 self.request_id.as_ref().ok_or(MissingFieldError::with_hint("request_id", "can be set using .with_request_id(...), and can be generated by boundless_market::Client"))
555 }
556
557 pub fn with_request_id(self, value: impl Into<RequestId>) -> Self {
562 Self { request_id: Some(value.into()), ..self }
563 }
564
565 pub fn with_offer(self, value: impl Into<OfferParams>) -> Self {
580 Self { offer: value.into(), ..self }
581 }
582
583 pub fn with_requirements(self, value: impl Into<RequirementParams>) -> Self {
595 Self { requirements: value.into(), ..self }
596 }
597
598 pub fn with_groth16_proof(self) -> Self {
603 let mut requirements = self.requirements;
604 requirements.selector = match crate::util::is_dev_mode() {
605 true => Some((SelectorExt::FakeReceipt as u32).into()),
606 false => Some((SelectorExt::groth16_latest() as u32).into()),
607 };
608 Self { requirements, ..self }
609 }
610
611 pub fn with_blake3_groth16_proof(self) -> Self {
616 let mut requirements = self.requirements;
617 requirements.selector = match crate::util::is_dev_mode() {
618 true => Some((SelectorExt::FakeReceipt as u32).into()),
619 false => Some((SelectorExt::blake3_groth16_latest() as u32).into()),
620 };
621 Self { requirements, ..self }
623 }
624}
625
626impl Debug for RequestParams {
627 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
629 f.debug_struct("ExampleRequestParams")
630 .field("program", &self.program.as_ref().map(|x| format!("[{} bytes]", x.len())))
631 .field("env", &self.env)
632 .field("program_url", &self.program_url)
633 .field("input", &self.request_input)
634 .field("cycles", &self.cycles)
635 .field("journal", &self.journal)
636 .field("request_id", &self.request_id)
637 .field("offer", &self.offer)
638 .field("requirements", &self.requirements)
639 .finish()
640 }
641}
642
643impl<Program, Env> From<(Program, Env)> for RequestParams
644where
645 Program: Into<Cow<'static, [u8]>>,
646 Env: Into<GuestEnv>,
647{
648 fn from(value: (Program, Env)) -> Self {
649 Self::default().with_program(value.0).with_env(value.1)
650 }
651}
652
653#[derive(Debug)]
658pub struct MissingFieldError {
659 pub label: Cow<'static, str>,
661 pub hint: Option<Cow<'static, str>>,
663}
664
665impl Display for MissingFieldError {
666 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667 match self.hint {
668 None => write!(f, "field `{}` is required but is uninitialized", self.label),
669 Some(ref hint) => {
670 write!(f, "field `{}` is required but is uninitialized; {hint}", self.label)
671 }
672 }
673 }
674}
675
676impl std::error::Error for MissingFieldError {}
677
678impl MissingFieldError {
679 pub fn new(label: impl Into<Cow<'static, str>>) -> Self {
681 Self { label: label.into(), hint: None }
682 }
683
684 pub fn with_hint(
686 label: impl Into<Cow<'static, str>>,
687 hint: impl Into<Cow<'static, str>>,
688 ) -> Self {
689 Self { label: label.into(), hint: Some(hint.into()) }
690 }
691}
692
693#[derive(Debug, Clone, Copy, PartialEq)]
699#[non_exhaustive]
700pub struct ParameterizationMode {
701 executor_speed: u64,
703 base_timeout: u32,
705 base_ramp_up_delay: u64,
707 base_ramp_up_period: u32,
709 timeout_seconds_per_mcycle: u32,
711 ramp_up_seconds_per_mcycle: u32,
713}
714
715impl ParameterizationMode {
716 const DEFAULT_EXECUTOR_SPEED_HZ: u64 = 1000000; const DEFAULT_BASE_TIMEOUT: u32 = 1800; const DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE: u32 = 7;
729
730 const DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE: u32 = 2;
734
735 const DEFAULT_BASE_RAMP_UP_PERIOD: u32 = 600; const DEFAULT_BASE_RAMP_UP_DELAY: u64 = 30;
746
747 pub fn fulfillment() -> Self {
756 Self {
757 executor_speed: Self::DEFAULT_EXECUTOR_SPEED_HZ,
758 base_timeout: Self::DEFAULT_BASE_TIMEOUT,
759 timeout_seconds_per_mcycle: Self::DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE,
760 ramp_up_seconds_per_mcycle: Self::DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE,
761 base_ramp_up_delay: Self::DEFAULT_BASE_RAMP_UP_DELAY,
762 base_ramp_up_period: Self::DEFAULT_BASE_RAMP_UP_PERIOD,
763 }
764 }
765
766 fn recommended_ramp_up_start(&self, cycle_count: Option<u64>) -> u64 {
771 cycle_count
772 .filter(|&count| count > 0)
773 .map(|cycle_count| {
774 now_timestamp()
775 + self.base_ramp_up_delay.max(self.executor_time(Some(cycle_count)) as u64)
776 })
777 .unwrap_or(now_timestamp() + self.base_ramp_up_delay)
778 }
779
780 fn recommended_ramp_up_period(&self, cycle_count: Option<u64>) -> u32 {
785 const MAX_RAMP_UP_PERIOD: u32 = 7200; cycle_count
787 .filter(|&count| count > 0)
788 .map(|cycle_count| {
789 let m_cycles = cycle_count.div_ceil(1_000_000) as u32;
790 m_cycles
791 .saturating_mul(self.ramp_up_seconds_per_mcycle)
792 .saturating_add(self.base_ramp_up_period)
793 .min(MAX_RAMP_UP_PERIOD)
794 })
795 .unwrap_or(self.base_ramp_up_period)
796 }
797
798 fn recommended_timeout(&self, cycle_count: Option<u64>) -> u32 {
808 const MAX_TIMEOUT: u32 = 14400; cycle_count
810 .filter(|&count| count > 0)
811 .map(|cycle_count| {
812 let m_cycles = cycle_count.div_ceil(1_000_000) as u32;
813 m_cycles
814 .saturating_mul(self.timeout_seconds_per_mcycle)
815 .saturating_add(self.base_timeout)
816 .min(MAX_TIMEOUT)
817 })
818 .unwrap_or(self.base_timeout)
819 }
820
821 fn executor_time(&self, cycle_count: Option<u64>) -> u32 {
825 cycle_count
826 .filter(|&count| count > 0)
827 .map(|cycle_count| cycle_count.div_ceil(self.executor_speed) as u32)
828 .unwrap_or(0)
829 }
830}
831
832impl Default for ParameterizationMode {
833 fn default() -> Self {
834 Self {
835 executor_speed: Self::DEFAULT_EXECUTOR_SPEED_HZ,
836 base_timeout: Self::DEFAULT_BASE_TIMEOUT,
837 timeout_seconds_per_mcycle: Self::DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE,
838 ramp_up_seconds_per_mcycle: Self::DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE,
839 base_ramp_up_delay: Self::DEFAULT_BASE_RAMP_UP_DELAY,
840 base_ramp_up_period: Self::DEFAULT_BASE_RAMP_UP_PERIOD,
841 }
842 }
843}
844
845#[cfg(test)]
846mod parameterization_mode_tests {
847 use super::ParameterizationMode;
848
849 #[test]
850 fn test_default_creation() {
851 let mode = ParameterizationMode::default();
852 assert_eq!(mode.executor_speed, ParameterizationMode::DEFAULT_EXECUTOR_SPEED_HZ);
853 assert_eq!(mode.base_timeout, ParameterizationMode::DEFAULT_BASE_TIMEOUT);
854 assert_eq!(mode.base_ramp_up_delay, ParameterizationMode::DEFAULT_BASE_RAMP_UP_DELAY);
855 assert_eq!(mode.base_ramp_up_period, ParameterizationMode::DEFAULT_BASE_RAMP_UP_PERIOD);
856 assert_eq!(
857 mode.timeout_seconds_per_mcycle,
858 ParameterizationMode::DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE
859 );
860 assert_eq!(
861 mode.ramp_up_seconds_per_mcycle,
862 ParameterizationMode::DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE
863 );
864 }
865
866 #[test]
867 fn test_fulfillment_creation() {
868 let mode = ParameterizationMode::fulfillment();
869 assert_eq!(mode.executor_speed, ParameterizationMode::DEFAULT_EXECUTOR_SPEED_HZ);
870 assert_eq!(mode.base_timeout, ParameterizationMode::DEFAULT_BASE_TIMEOUT);
871 assert_eq!(mode.base_ramp_up_delay, ParameterizationMode::DEFAULT_BASE_RAMP_UP_DELAY);
872 assert_eq!(mode.base_ramp_up_period, ParameterizationMode::DEFAULT_BASE_RAMP_UP_PERIOD);
873 assert_eq!(
874 mode.timeout_seconds_per_mcycle,
875 ParameterizationMode::DEFAULT_TIMEOUT_SECONDS_PER_MCYCLE
876 );
877 assert_eq!(
878 mode.ramp_up_seconds_per_mcycle,
879 ParameterizationMode::DEFAULT_RAMP_UP_SECONDS_PER_MCYCLE
880 );
881 }
882
883 #[test]
884 fn test_recommended_timeout_default() {
885 let mode = ParameterizationMode::default();
886
887 let cycle_count = 1_000_000; let timeout = mode.recommended_timeout(Some(cycle_count));
890
891 assert_eq!(timeout, 1807);
893
894 let cycle_count = 50_000_000; let timeout = mode.recommended_timeout(Some(cycle_count));
897
898 assert_eq!(timeout, 2150);
900 }
901
902 #[test]
903 fn test_recommended_timeout_minimum() {
904 let mode = ParameterizationMode::default();
905
906 let timeout = mode.recommended_timeout(Some(0));
908 assert_eq!(timeout, mode.base_timeout);
909
910 let timeout = mode.recommended_timeout(Some(100));
912 assert_eq!(timeout, mode.base_timeout + mode.timeout_seconds_per_mcycle);
913 }
914
915 #[test]
916 fn test_recommended_ramp_up_period_default() {
917 let mode = ParameterizationMode::default();
918
919 let cycle_count = 1_000_000;
921 let ramp_up = mode.recommended_ramp_up_period(Some(cycle_count as u64));
922
923 assert_eq!(ramp_up, 602);
925
926 let cycle_count = 50_000_000;
928 let ramp_up = mode.recommended_ramp_up_period(Some(cycle_count as u64));
929
930 assert_eq!(ramp_up, 700);
932 }
933
934 #[test]
935 fn test_recommended_ramp_up_period_zero_cycles() {
936 let mode = ParameterizationMode::default();
937
938 let ramp_up = mode.recommended_ramp_up_period(Some(0));
940 assert_eq!(ramp_up, ParameterizationMode::DEFAULT_BASE_RAMP_UP_PERIOD);
941 }
942
943 #[test]
944 fn test_recommended_ramp_up_start_default() {
945 let mode = ParameterizationMode::default();
946 let now = crate::util::now_timestamp();
947
948 let ramp_up_start = mode.recommended_ramp_up_start(Some(0));
950 assert!(ramp_up_start >= now + mode.base_ramp_up_delay);
951 assert!(ramp_up_start <= now + mode.base_ramp_up_delay + 1); let ramp_up_start = mode.recommended_ramp_up_start(None);
955 assert!(ramp_up_start >= now + mode.base_ramp_up_delay);
956 assert!(ramp_up_start <= now + mode.base_ramp_up_delay + 1); let cycle_count = 1_000_000;
960 let executor_time = mode.executor_time(Some(cycle_count));
961 let expected_delay = mode.base_ramp_up_delay.max(executor_time as u64);
962 let ramp_up_start = mode.recommended_ramp_up_start(Some(cycle_count));
963 assert!(ramp_up_start >= now + expected_delay);
964 assert!(ramp_up_start <= now + expected_delay + 1); let cycle_count = 50_000_000;
968 let executor_time = mode.executor_time(Some(cycle_count));
969 let expected_delay = mode.base_ramp_up_delay.max(executor_time as u64);
970 let ramp_up_start = mode.recommended_ramp_up_start(Some(cycle_count));
971 assert!(ramp_up_start >= now + expected_delay);
972 assert!(ramp_up_start <= now + expected_delay + 1); }
974}
975
976#[cfg(test)]
977mod tests {
978 use std::sync::Arc;
979
980 use alloy::{
981 network::TransactionBuilder,
982 node_bindings::Anvil,
983 primitives::Address,
984 providers::{DynProvider, Provider},
985 rpc::types::TransactionRequest,
986 };
987 use boundless_test_utils::{guests::ECHO_ELF, market::create_test_ctx};
988 use tracing_test::traced_test;
989 use url::Url;
990
991 use super::{
992 Adapt, Layer, OfferLayer, OfferLayerConfig, OfferParams, ParameterizationMode,
993 PreflightLayer, RequestBuilder, RequestId, RequestIdLayer, RequestIdLayerConfig,
994 RequestIdLayerMode, RequestParams, RequirementsLayer, StandardRequestBuilder, StorageLayer,
995 StorageLayerConfig,
996 };
997
998 use crate::prover_utils::{local_executor::LocalExecutor, prover::Prover};
999 use crate::storage::HttpDownloader;
1000 use crate::{
1001 contracts::{
1002 boundless_market::BoundlessMarketService, FulfillmentData, Predicate, RequestInput,
1003 RequestInputType, Requirements,
1004 },
1005 input::GuestEnv,
1006 storage::{MockStorageUploader, StandardDownloader, StorageDownloader, StorageUploader},
1007 util::NotProvided,
1008 StandardUploader,
1009 };
1010 use alloy_primitives::{utils::parse_ether, U256};
1011 use risc0_zkvm::{compute_image_id, sha::Digestible, Journal};
1012
1013 #[tokio::test]
1014 #[traced_test]
1015 async fn basic() -> anyhow::Result<()> {
1016 let anvil = Anvil::new().spawn();
1017 let test_ctx = create_test_ctx(&anvil).await.unwrap();
1018 let uploader = Arc::new(MockStorageUploader::new());
1019 let downloader = HttpDownloader::new(None, None);
1020 let market = BoundlessMarketService::new(
1021 test_ctx.deployment.boundless_market_address,
1022 test_ctx.customer_provider.clone(),
1023 test_ctx.customer_signer.address(),
1024 );
1025
1026 let request_builder = StandardRequestBuilder::builder()
1027 .storage_layer(Some(uploader))
1028 .preflight_layer(PreflightLayer::new(LocalExecutor::default(), Some(downloader)))
1029 .offer_layer(test_ctx.customer_provider.clone())
1030 .request_id_layer(market)
1031 .build()?;
1032
1033 let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
1034 let request = request_builder.build(params).await?;
1035 println!("built request {request:#?}");
1036 Ok(())
1037 }
1038
1039 #[tokio::test]
1040 #[traced_test]
1041 async fn offer_layer_lock_collateral_default() -> anyhow::Result<()> {
1042 let anvil = Anvil::new().spawn();
1043 let test_ctx = create_test_ctx(&anvil).await.unwrap();
1044 let storage = Arc::new(MockStorageUploader::new());
1045 let downloader = HttpDownloader::new(None, None);
1046 let market = BoundlessMarketService::new(
1047 test_ctx.deployment.boundless_market_address,
1048 test_ctx.customer_provider.clone(),
1049 test_ctx.customer_signer.address(),
1050 );
1051
1052 let request_builder = StandardRequestBuilder::builder()
1053 .storage_layer(Some(storage))
1054 .preflight_layer(PreflightLayer::new(LocalExecutor::default(), Some(downloader)))
1055 .offer_layer(OfferLayer::new(
1056 test_ctx.customer_provider.clone(),
1057 OfferLayerConfig::builder().build()?,
1058 ))
1059 .request_id_layer(market)
1060 .build()?;
1061
1062 let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
1063 let request = request_builder.build(params).await?;
1064 assert_eq!(request.offer.lockCollateral, parse_ether("5").unwrap());
1065 Ok(())
1066 }
1067
1068 #[tokio::test]
1069 #[traced_test]
1070 async fn with_offer_layer_settings() -> anyhow::Result<()> {
1071 let anvil = Anvil::new().spawn();
1072 let test_ctx = create_test_ctx(&anvil).await.unwrap();
1073 let uploader = Arc::new(MockStorageUploader::new());
1074 let downloader = HttpDownloader::new(None, None);
1075 let market = BoundlessMarketService::new(
1076 test_ctx.deployment.boundless_market_address,
1077 test_ctx.customer_provider.clone(),
1078 test_ctx.customer_signer.address(),
1079 );
1080
1081 let request_builder = StandardRequestBuilder::builder()
1082 .storage_layer(Some(uploader))
1083 .preflight_layer(PreflightLayer::new(LocalExecutor::default(), Some(downloader)))
1084 .offer_layer(OfferLayer::new(
1085 test_ctx.customer_provider.clone(),
1086 OfferLayerConfig::builder()
1087 .ramp_up_period(27)
1088 .lock_collateral(parse_ether("10").unwrap())
1089 .build()?,
1090 ))
1091 .request_id_layer(market)
1092 .build()?;
1093
1094 let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
1095 let request = request_builder.build(params).await?;
1096 assert_eq!(request.offer.rampUpPeriod, 27);
1097 assert_eq!(request.offer.lockCollateral, parse_ether("10").unwrap());
1098 Ok(())
1099 }
1100
1101 #[tokio::test]
1102 #[traced_test]
1103 async fn without_storage_uploader() -> anyhow::Result<()> {
1104 let anvil = Anvil::new().spawn();
1105 let test_ctx = create_test_ctx(&anvil).await.unwrap();
1106 let downloader = HttpDownloader::new(None, None);
1107 let market = BoundlessMarketService::new(
1108 test_ctx.deployment.boundless_market_address,
1109 test_ctx.customer_provider.clone(),
1110 test_ctx.customer_signer.address(),
1111 );
1112
1113 let request_builder = StandardRequestBuilder::builder::<_, NotProvided, _>()
1114 .preflight_layer(PreflightLayer::new(LocalExecutor::default(), Some(downloader)))
1115 .offer_layer(test_ctx.customer_provider.clone())
1116 .request_id_layer(market)
1117 .build()?;
1118
1119 let params = request_builder.params().with_program(ECHO_ELF).with_stdin(b"hello!");
1121 let err = request_builder.build(params).await.unwrap_err();
1122 tracing::debug!("err: {err}");
1123
1124 let uploader = Arc::new(MockStorageUploader::new());
1126 let program_url = uploader.upload_program(ECHO_ELF).await?;
1127 let params = request_builder.params().with_program_url(program_url)?.with_stdin(b"hello!");
1128 let request = request_builder.build(params).await?;
1129 let predicate = Predicate::try_from(request.requirements.predicate.clone())?;
1130 assert_eq!(predicate.image_id().unwrap(), risc0_zkvm::compute_image_id(ECHO_ELF)?);
1131 Ok(())
1132 }
1133
1134 #[tokio::test]
1135 #[traced_test]
1136 async fn test_storage_layer() -> anyhow::Result<()> {
1137 let uploader = Arc::new(MockStorageUploader::new());
1138 let downloader = HttpDownloader::new(None, None);
1139 let layer = StorageLayer::new(
1140 Some(uploader),
1141 StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
1142 );
1143 let env = GuestEnv::from_stdin(b"inline_data");
1144 let (program_url, request_input) = layer.process((ECHO_ELF, &env)).await?;
1145
1146 assert_eq!(downloader.download_url(program_url).await?, ECHO_ELF);
1148 assert_eq!(request_input.inputType, RequestInputType::Inline);
1149 assert_eq!(request_input.data, env.encode()?);
1150 Ok(())
1151 }
1152
1153 #[tokio::test]
1154 #[traced_test]
1155 async fn test_storage_layer_no_provider() -> anyhow::Result<()> {
1156 let layer = StorageLayer::<NotProvided>::from(
1157 StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
1158 );
1159
1160 let env = GuestEnv::from_stdin(b"inline_data");
1161 let request_input = layer.process(&env).await?;
1162
1163 assert_eq!(request_input.inputType, RequestInputType::Inline);
1165 assert_eq!(request_input.data, env.encode()?);
1166 Ok(())
1167 }
1168
1169 #[tokio::test]
1170 #[traced_test]
1171 async fn test_storage_layer_large_input() -> anyhow::Result<()> {
1172 let uploader = Arc::new(MockStorageUploader::new());
1173 let downloader = HttpDownloader::new(None, None);
1174 let layer = StorageLayer::new(
1175 Some(uploader),
1176 StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
1177 );
1178 let env = GuestEnv::from_stdin(rand::random_iter().take(2048).collect::<Vec<u8>>());
1179 let (program_url, request_input) = layer.process((ECHO_ELF, &env)).await?;
1180
1181 assert_eq!(downloader.download_url(program_url).await?, ECHO_ELF);
1183 assert_eq!(request_input.inputType, RequestInputType::Url);
1184 let fetched_input =
1185 downloader.download(&String::from_utf8(request_input.data.to_vec())?).await?;
1186 assert_eq!(fetched_input, env.encode()?);
1187 Ok(())
1188 }
1189
1190 #[tokio::test]
1191 #[traced_test]
1192 async fn test_storage_layer_large_input_no_provider() -> anyhow::Result<()> {
1193 let layer = StorageLayer::from(
1194 StorageLayerConfig::builder().inline_input_max_bytes(Some(1024)).build()?,
1195 );
1196
1197 let env = GuestEnv::from_stdin(rand::random_iter().take(2048).collect::<Vec<u8>>());
1198 let err = layer.process(&env).await.unwrap_err();
1199
1200 assert!(err
1201 .to_string()
1202 .contains("cannot upload input using StorageLayer with no storage_uploader"));
1203 Ok(())
1204 }
1205
1206 #[tokio::test]
1207 #[traced_test]
1208 async fn test_preflight_layer() -> anyhow::Result<()> {
1209 let uploader = MockStorageUploader::new();
1210 let downloader = HttpDownloader::new(None, None);
1211 let program_url = uploader.upload_program(ECHO_ELF).await?;
1212 let layer = PreflightLayer::new(LocalExecutor::default(), Some(downloader));
1213 let data = b"hello_zkvm".to_vec();
1214 let env = GuestEnv::from_stdin(data.clone());
1215 let input = RequestInput::inline(env.encode()?);
1216
1217 let params = RequestParams::new().with_program_url(program_url)?.with_request_input(input);
1219 let result = params.process_with(&layer).await?;
1220
1221 assert_eq!(result.journal.unwrap().bytes, data);
1222 assert!(result.cycles.unwrap() > 0);
1224 Ok(())
1225 }
1226
1227 #[tokio::test]
1228 #[traced_test]
1229 async fn test_preflight_layer_cache_prefill() -> anyhow::Result<()> {
1230 let layer: PreflightLayer<HttpDownloader> = PreflightLayer::default();
1232 let executor = layer.executor_cloned();
1233
1234 let image_id = risc0_zkvm::compute_image_id(ECHO_ELF)?;
1236 let input_data = b"test input";
1237 let cycles = 9999999u64;
1238 let journal = risc0_zkvm::Journal::new(input_data.to_vec());
1239
1240 let env = GuestEnv { stdin: input_data.to_vec(), ..Default::default() };
1242 let request_input =
1243 RequestInput { inputType: RequestInputType::Inline, data: env.encode()?.into() };
1244
1245 let params = RequestParams::new()
1246 .with_image_id(image_id)
1247 .with_cycles(cycles)
1248 .with_journal(journal.clone())
1249 .with_request_input(request_input);
1250
1251 let result = params.process_with(&layer).await?;
1253
1254 assert_eq!(result.cycles, Some(cycles));
1256 assert_eq!(result.journal, Some(journal));
1257
1258 executor.upload_image(&image_id.to_string(), ECHO_ELF.to_vec()).await?;
1261 let input_id = executor.upload_input(input_data.to_vec()).await?;
1262
1263 let preflight_result =
1264 executor.preflight(&image_id.to_string(), &input_id, vec![], None, "test").await?;
1265
1266 assert_eq!(preflight_result.stats.total_cycles, cycles);
1267
1268 Ok(())
1269 }
1270
1271 #[tokio::test]
1272 #[traced_test]
1273 async fn test_requirements_layer() -> anyhow::Result<()> {
1274 let layer = RequirementsLayer::default();
1275 let program = ECHO_ELF;
1276 let bytes = b"journal_data".to_vec();
1277 let journal = Journal::new(bytes.clone());
1278 let req = layer.process((program, &journal, &Default::default())).await?;
1279 let predicate = Predicate::try_from(req.predicate.clone())?;
1280 let fulfillment_data = FulfillmentData::from_image_id_and_journal(
1281 predicate.image_id().unwrap(),
1282 journal.bytes.clone(),
1283 );
1284 assert!(predicate.eval(&fulfillment_data).is_some());
1286 let other = Journal::new(b"other_data".to_vec());
1288 let fulfillment_data = FulfillmentData::from_image_id_and_journal(
1289 predicate.image_id().unwrap(),
1290 other.bytes.clone(),
1291 );
1292 assert!(predicate.eval(&fulfillment_data).is_none());
1293 Ok(())
1294 }
1295
1296 #[tokio::test]
1297 #[traced_test]
1298 async fn test_request_id_layer_rand() -> anyhow::Result<()> {
1299 let anvil = Anvil::new().spawn();
1300 let test_ctx = create_test_ctx(&anvil).await?;
1301 let market = BoundlessMarketService::new(
1302 test_ctx.deployment.boundless_market_address,
1303 test_ctx.customer_provider.clone(),
1304 test_ctx.customer_signer.address(),
1305 );
1306 let layer = RequestIdLayer::from(market.clone());
1307 assert_eq!(layer.config.mode, RequestIdLayerMode::Rand);
1308 let id = layer.process(()).await?;
1309 assert_eq!(id.addr, test_ctx.customer_signer.address());
1310 assert!(!id.smart_contract_signed);
1311 Ok(())
1312 }
1313
1314 #[tokio::test]
1315 #[traced_test]
1316 async fn test_request_id_layer_nonce() -> anyhow::Result<()> {
1317 let anvil = Anvil::new().spawn();
1318 let test_ctx = create_test_ctx(&anvil).await?;
1319 let market = BoundlessMarketService::new(
1320 test_ctx.deployment.boundless_market_address,
1321 test_ctx.customer_provider.clone(),
1322 test_ctx.customer_signer.address(),
1323 );
1324 let layer = RequestIdLayer::new(
1325 market.clone(),
1326 RequestIdLayerConfig::builder().mode(RequestIdLayerMode::Nonce).build()?,
1327 );
1328
1329 let id = layer.process(()).await?;
1330 assert_eq!(id.addr, test_ctx.customer_signer.address());
1331 assert_eq!(id.index, 0);
1333 assert!(!id.smart_contract_signed);
1334
1335 let tx = TransactionRequest::default()
1337 .with_from(test_ctx.customer_signer.address())
1338 .with_to(Address::ZERO)
1339 .with_value(U256::from(1));
1340 test_ctx.customer_provider.send_transaction(tx).await?.watch().await?;
1341
1342 let id = layer.process(()).await?;
1343 assert_eq!(id.addr, test_ctx.customer_signer.address());
1344 assert_eq!(id.index, 1);
1346 assert!(!id.smart_contract_signed);
1347
1348 Ok(())
1349 }
1350
1351 #[tokio::test]
1352 #[traced_test]
1353 async fn test_offer_layer_estimates() -> anyhow::Result<()> {
1354 let anvil = Anvil::new().spawn();
1356 let test_ctx = create_test_ctx(&anvil).await?;
1357 let provider = test_ctx.customer_provider.clone();
1358 let layer = OfferLayer::from(provider.clone());
1359 let image_id = compute_image_id(ECHO_ELF).unwrap();
1361 let predicate = Predicate::digest_match(image_id, Journal::new(b"hello".to_vec()).digest());
1362 let requirements = Requirements::new(predicate);
1363 let request_id = RequestId::new(test_ctx.customer_signer.address(), 0);
1364
1365 let offer_params = OfferParams::default();
1367 let now = crate::util::now_timestamp();
1368 let offer_zero_mcycles =
1369 layer.process((&requirements, &request_id, Some(0u64), &offer_params)).await?;
1370 assert_eq!(offer_zero_mcycles.minPrice, U256::ZERO);
1371 assert_eq!(
1373 offer_zero_mcycles.rampUpPeriod,
1374 ParameterizationMode::DEFAULT_BASE_RAMP_UP_PERIOD
1375 );
1376 assert_eq!(offer_zero_mcycles.lockTimeout, ParameterizationMode::DEFAULT_BASE_TIMEOUT);
1377 assert_eq!(offer_zero_mcycles.timeout, ParameterizationMode::DEFAULT_BASE_TIMEOUT * 2);
1378 assert!(
1380 offer_zero_mcycles.rampUpStart
1381 >= now + ParameterizationMode::DEFAULT_BASE_RAMP_UP_DELAY
1382 );
1383 assert!(
1384 offer_zero_mcycles.rampUpStart
1385 <= now + ParameterizationMode::DEFAULT_BASE_RAMP_UP_DELAY + 1
1386 ); assert!(offer_zero_mcycles.maxPrice > U256::ZERO);
1389
1390 let offer_more_mcycles =
1392 layer.process((&requirements, &request_id, Some(100u64 << 20), &offer_params)).await?;
1393 assert!(offer_more_mcycles.maxPrice > offer_zero_mcycles.maxPrice);
1394
1395 let min_price = U256::from(1u64);
1397 let max_price = U256::from(5u64);
1398 let bidding_start = now + 100;
1399 let offer_params = OfferParams::builder()
1400 .max_price(max_price)
1401 .min_price(min_price)
1402 .bidding_start(bidding_start)
1403 .ramp_up_period(20)
1404 .lock_timeout(50)
1405 .timeout(80)
1406 .into();
1407 let offer_zero_mcycles =
1408 layer.process((&requirements, &request_id, Some(0u64), &offer_params)).await?;
1409 assert_eq!(offer_zero_mcycles.maxPrice, max_price);
1410 assert_eq!(offer_zero_mcycles.minPrice, min_price);
1411 assert_eq!(offer_zero_mcycles.rampUpPeriod, 20);
1412 assert_eq!(offer_zero_mcycles.lockTimeout, 50);
1413 assert_eq!(offer_zero_mcycles.timeout, 80);
1414 assert_eq!(offer_zero_mcycles.rampUpStart, bidding_start);
1415 Ok(())
1416 }
1417
1418 #[tokio::test]
1419 #[traced_test]
1420 async fn test_offer_layer_with_parameterization_mode() -> anyhow::Result<()> {
1421 let anvil = Anvil::new().spawn();
1423 let test_ctx = create_test_ctx(&anvil).await?;
1424 let provider = test_ctx.customer_provider.clone();
1425
1426 let image_id = compute_image_id(ECHO_ELF).unwrap();
1428 let predicate = Predicate::digest_match(image_id, Journal::new(b"hello".to_vec()).digest());
1429 let requirements = Requirements::new(predicate);
1430 let request_id = RequestId::new(test_ctx.customer_signer.address(), 0);
1431
1432 let fulfillment_mode = ParameterizationMode::fulfillment();
1434 let layer = OfferLayer::new(
1435 provider.clone(),
1436 OfferLayerConfig::builder().parameterization_mode(fulfillment_mode).build()?,
1437 );
1438 let now = crate::util::now_timestamp();
1439 let cycle_count = 100_000_000; let offer_params = OfferParams::default();
1441 let offer =
1442 layer.process((&requirements, &request_id, Some(cycle_count), &offer_params)).await?;
1443
1444 let expected_executor_time = fulfillment_mode.executor_time(Some(cycle_count));
1446 let expected_delay = fulfillment_mode.base_ramp_up_delay.max(expected_executor_time as u64);
1447 assert!(offer.rampUpStart >= now + expected_delay);
1448 assert!(offer.rampUpStart <= now + expected_delay + 1); let expected_ramp_up_period =
1452 fulfillment_mode.recommended_ramp_up_period(Some(cycle_count));
1453 assert_eq!(offer.rampUpPeriod, expected_ramp_up_period);
1454
1455 Ok(())
1456 }
1457
1458 #[test]
1459 fn request_params_with_program_url_infallible() {
1460 let url = Url::parse("https://fileserver.example/guest.bin").unwrap();
1464 RequestParams::new().with_program_url(url).inspect_err(|e| match *e {}).unwrap();
1465 }
1466
1467 #[test]
1468 fn request_params_with_input_url_infallible() {
1469 let url = Url::parse("https://fileserver.example/input.bin").unwrap();
1473 RequestParams::new().with_input_url(url).inspect_err(|e| match *e {}).unwrap();
1474 }
1475
1476 #[test]
1477 fn test_with_input_url() {
1478 let params =
1480 RequestParams::new().with_input_url("https://fileserver.example/input.bin").unwrap();
1481
1482 let input = params.request_input.unwrap();
1483 assert_eq!(input.inputType, RequestInputType::Url);
1484 assert_eq!(input.data.as_ref(), "https://fileserver.example/input.bin".as_bytes());
1485
1486 let url = Url::parse("https://fileserver.example/input2.bin").unwrap();
1488 let params = RequestParams::new().with_input_url(url).unwrap();
1489
1490 let input = params.request_input.unwrap();
1491 assert_eq!(input.inputType, RequestInputType::Url);
1492 assert_eq!(input.data.as_ref(), "https://fileserver.example/input2.bin".as_bytes());
1493 }
1494
1495 #[allow(dead_code)]
1496 trait AssertSend: Send {}
1497
1498 impl AssertSend for StandardRequestBuilder<DynProvider, StandardUploader, StandardDownloader> {}
1500}