From d17b751e0ef410d27d72f1c4df0e80c49ab9d797 Mon Sep 17 00:00:00 2001 From: capossele Date: Wed, 18 Feb 2026 14:05:36 +0000 Subject: [PATCH 1/9] add pricing override --- .gitignore | 1 + ansible/roles/prover/defaults/main.yml | 2 + ansible/roles/prover/tasks/main.yml | 9 + broker-template.toml | 21 ++ compose.yml | 4 + .../src/commands/prover/generate_config.rs | 12 + .../src/prover_utils/config.rs | 271 +++++++++++++++++- .../boundless-market/src/prover_utils/mod.rs | 21 +- crates/broker/src/lib.rs | 108 +++++++ crates/broker/src/order_picker.rs | 186 ++++++++++++ justfile | 6 + pricing-overrides.template.json | 5 + 12 files changed, 640 insertions(+), 6 deletions(-) create mode 100644 pricing-overrides.template.json diff --git a/.gitignore b/.gitignore index 73abe0014..4af614919 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ deployment_secrets.toml # Ignore broker config file(s) *broker*.toml +pricing-overrides.json ansible/inventory.yml # Cargo diff --git a/ansible/roles/prover/defaults/main.yml b/ansible/roles/prover/defaults/main.yml index 401964b27..13f042c54 100644 --- a/ansible/roles/prover/defaults/main.yml +++ b/ansible/roles/prover/defaults/main.yml @@ -64,5 +64,7 @@ prover_povw_log_id: "" # Broker configuration URL prover_broker_toml_url: "https://raw.githubusercontent.com/boundless-xyz/boundless/refs/heads/main/broker-template.toml" +# Pricing overrides JSON (default ships with empty overrides; edit on host to take effect within 60s) +prover_pricing_overrides_url: "https://raw.githubusercontent.com/boundless-xyz/boundless/refs/heads/main/pricing-overrides.template.json" prover_docker_compose_profile: "--profile broker --profile miner" prover_docker_compose_invoke: "" diff --git a/ansible/roles/prover/tasks/main.yml b/ansible/roles/prover/tasks/main.yml index df096d630..52878a102 100644 --- a/ansible/roles/prover/tasks/main.yml +++ b/ansible/roles/prover/tasks/main.yml @@ -27,6 +27,15 @@ mode: '0644' notify: Restart Bento +- name: Download pricing overrides file (creates default if missing) + ansible.builtin.get_url: + url: '{{ prover_pricing_overrides_url }}' + dest: "{{ prover_dir }}/pricing-overrides.json" + owner: root + group: root + mode: '0644' + force: false + - name: Write the system service file ansible.builtin.template: src: bento.service.j2 diff --git a/broker-template.toml b/broker-template.toml index 0bb6c6c91..0b7c85937 100644 --- a/broker-template.toml +++ b/broker-template.toml @@ -25,6 +25,27 @@ # `min_mcycle_price` to determine if the order should be locked. min_mcycle_price = "0.00002 USD" +# Optional path to a JSON file with per-requestor and per-selector pricing overrides. +# When set, the broker checks the file for a matching override before falling back to +# the global min_mcycle_price above. See the example file format: +# +# { +# "by_requestor": { +# "0xAbC...123": { "min_mcycle_price": "0.0001 USD" } +# }, +# "by_selector": { +# "0x12345678": { "min_mcycle_price": "0.0005 USD" } +# }, +# "by_requestor_selector": { +# "0xAbC...123:0x12345678": { "min_mcycle_price": "0.001 USD" } +# } +# } +# +# Resolution priority: requestor+selector > selector > requestor > global default. +# The file is automatically hot-reloaded every 60 seconds; no broker restart needed. +# Copy pricing-overrides.template.json to pricing-overrides.json to get started. +pricing_overrides_path = "pricing-overrides.json" + # The minimum price per mega-cycle (i.e. million RISC-V cycles) for the broker # to attempt to fulfill an order in the case that the order was locked by another prover # but not fulfilled within the lock timeout. diff --git a/compose.yml b/compose.yml index 456eb8826..3e097e850 100644 --- a/compose.yml +++ b/compose.yml @@ -278,10 +278,14 @@ services: broker: <<: *broker-common + working_dir: /app # needed for source-build Dockerfile which doesn't set WORKDIR in the runtime stage volumes: - type: bind source: ./broker.toml target: /app/broker.toml + - type: bind + source: ./pricing-overrides.json + target: /app/pricing-overrides.json - broker-data:/db/ # Uncomment when using locally built set-builder and assessor guest programs # - type: bind diff --git a/crates/boundless-cli/src/commands/prover/generate_config.rs b/crates/boundless-cli/src/commands/prover/generate_config.rs index 14f47b5dc..ba2922961 100644 --- a/crates/boundless-cli/src/commands/prover/generate_config.rs +++ b/crates/boundless-cli/src/commands/prover/generate_config.rs @@ -135,6 +135,18 @@ impl ProverGenerateConfig { self.generate_broker_toml(&config, broker_strategy, &display)?; display.item_colored("Created", self.broker_toml_file.display(), "green"); + // Create pricing-overrides.json next to broker.toml if it doesn't exist + let pricing_overrides_path = + self.broker_toml_file.parent().unwrap_or(Path::new(".")).join("pricing-overrides.json"); + if !pricing_overrides_path.exists() { + std::fs::write( + &pricing_overrides_path, + include_str!("../../../../../pricing-overrides.template.json"), + ) + .context("Failed to write pricing-overrides.json")?; + display.item_colored("Created", pricing_overrides_path.display(), "green"); + } + // Backup and generate compose.yml if let Some(backup_path) = self.backup_file(&self.compose_yml_file)? { display.item_colored("Backup saved", backup_path.display(), "cyan"); diff --git a/crates/boundless-market/src/prover_utils/config.rs b/crates/boundless-market/src/prover_utils/config.rs index 11f3bd4db..4985ee8d1 100644 --- a/crates/boundless-market/src/prover_utils/config.rs +++ b/crates/boundless-market/src/prover_utils/config.rs @@ -21,9 +21,12 @@ #[cfg(any(feature = "prover_utils", feature = "test-utils"))] use std::path::Path; -use std::{collections::HashSet, path::PathBuf}; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; -use alloy::primitives::Address; +use alloy::primitives::{Address, FixedBytes}; #[cfg(any(feature = "prover_utils", feature = "test-utils"))] use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -499,6 +502,12 @@ pub struct MarketConfig { /// market. This should remain false to avoid losing partial PoVW jobs. #[serde(default)] pub cancel_proving_expired_orders: bool, + /// Optional path to a JSON file containing per-requestor and per-selector pricing overrides. + /// + /// The file is hot-reloaded automatically (every 60 s); no broker restart needed. + /// See [`PricingOverrides`] for the file format. + #[serde(default)] + pub pricing_overrides_path: Option, } impl Default for MarketConfig { @@ -545,6 +554,7 @@ impl Default for MarketConfig { order_pricing_priority: OrderPricingPriority::default(), order_commitment_priority: OrderCommitmentPriority::default(), cancel_proving_expired_orders: false, + pricing_overrides_path: None, } } } @@ -722,3 +732,260 @@ impl Config { fs::write(path, data).await.context("Failed to write Config to disk") } } + +/// A single pricing override entry. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PricingOverrideEntry { + /// Minimum price per mega-cycle for this override. + pub min_mcycle_price: Amount, +} + +/// Per-requestor and per-selector pricing overrides loaded from a JSON file. +/// +/// Resolution priority (first match wins): +/// 1. `by_requestor_selector` -- keyed by `"
:"` +/// 2. `by_selector` -- keyed by selector hex (e.g. `"0x12345678"`) +/// 3. `by_requestor` -- keyed by requestor address +/// 4. Fall back to global `MarketConfig::min_mcycle_price` +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct PricingOverrides { + /// Overrides keyed by requestor address (checksummed or lowercase hex). + #[serde(default)] + pub by_requestor: HashMap, + /// Overrides keyed by selector hex (e.g. `"0x12345678"`). + #[serde(default)] + pub by_selector: HashMap, PricingOverrideEntry>, + /// Overrides keyed by `":"` for the most specific match. + #[serde( + default, + deserialize_with = "deserialize_requestor_selector_map", + serialize_with = "serialize_requestor_selector_map" + )] + pub by_requestor_selector: HashMap<(Address, FixedBytes<4>), PricingOverrideEntry>, +} + +fn deserialize_requestor_selector_map<'de, D>( + deserializer: D, +) -> Result), PricingOverrideEntry>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let raw: HashMap = HashMap::deserialize(deserializer)?; + let mut map = HashMap::with_capacity(raw.len()); + for (key, entry) in raw { + let parts: Vec<&str> = key.split(':').collect(); + if parts.len() != 2 { + return Err(serde::de::Error::custom(format!( + "by_requestor_selector key must be '
:', got '{key}'" + ))); + } + let addr: Address = parts[0].parse().map_err(serde::de::Error::custom)?; + let sel: FixedBytes<4> = parts[1].parse().map_err(serde::de::Error::custom)?; + map.insert((addr, sel), entry); + } + Ok(map) +} + +fn serialize_requestor_selector_map( + map: &HashMap<(Address, FixedBytes<4>), PricingOverrideEntry>, + serializer: S, +) -> Result +where + S: serde::Serializer, +{ + use serde::ser::SerializeMap; + let mut ser_map = serializer.serialize_map(Some(map.len()))?; + for ((addr, sel), entry) in map { + ser_map.serialize_entry(&format!("{addr}:{sel}"), entry)?; + } + ser_map.end() +} + +impl PricingOverrides { + /// Load overrides from a JSON file. + #[cfg(any(feature = "prover_utils", feature = "test-utils"))] + pub async fn load(path: &Path) -> Result { + let data = fs::read_to_string(path) + .await + .context(format!("Failed to read pricing overrides from {path:?}"))?; + serde_json::from_str(&data) + .context(format!("Failed to parse pricing overrides from {path:?}")) + } + + /// Resolve the effective `min_mcycle_price` for a given requestor and selector. + /// + /// Returns `Some(amount)` if an override matches, `None` to use the global default. + pub fn resolve(&self, requestor: &Address, selector: &FixedBytes<4>) -> Option<&Amount> { + if let Some(entry) = self.by_requestor_selector.get(&(*requestor, *selector)) { + return Some(&entry.min_mcycle_price); + } + if let Some(entry) = self.by_selector.get(selector) { + return Some(&entry.min_mcycle_price); + } + if let Some(entry) = self.by_requestor.get(requestor) { + return Some(&entry.min_mcycle_price); + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn addr(s: &str) -> Address { + s.parse().unwrap() + } + + fn sel(s: &str) -> FixedBytes<4> { + s.parse().unwrap() + } + + fn amount(s: &str) -> Amount { + Amount::parse(s, None).unwrap() + } + + fn entry(s: &str) -> PricingOverrideEntry { + PricingOverrideEntry { min_mcycle_price: amount(s) } + } + + #[test] + fn test_resolve_empty_returns_none() { + let overrides = PricingOverrides::default(); + let result = overrides + .resolve(&addr("0x0000000000000000000000000000000000000001"), &sel("0x12345678")); + assert!(result.is_none()); + } + + #[test] + fn test_resolve_by_requestor() { + let requestor = addr("0x0000000000000000000000000000000000000001"); + let mut overrides = PricingOverrides::default(); + overrides.by_requestor.insert(requestor, entry("0.001 USD")); + + let result = overrides.resolve(&requestor, &sel("0xaabbccdd")); + assert_eq!(result.unwrap().to_string(), "0.001 USD"); + + let other = addr("0x0000000000000000000000000000000000000002"); + assert!(overrides.resolve(&other, &sel("0xaabbccdd")).is_none()); + } + + #[test] + fn test_resolve_by_selector() { + let selector = sel("0x12345678"); + let mut overrides = PricingOverrides::default(); + overrides.by_selector.insert(selector, entry("0.005 USD")); + + let any_addr = addr("0x0000000000000000000000000000000000000099"); + let result = overrides.resolve(&any_addr, &selector); + assert_eq!(result.unwrap().to_string(), "0.005 USD"); + + assert!(overrides.resolve(&any_addr, &sel("0x00000000")).is_none()); + } + + #[test] + fn test_resolve_priority_requestor_selector_over_individual() { + let requestor = addr("0x0000000000000000000000000000000000000001"); + let selector = sel("0x12345678"); + + let mut overrides = PricingOverrides::default(); + overrides.by_requestor.insert(requestor, entry("0.001 USD")); + overrides.by_selector.insert(selector, entry("0.002 USD")); + overrides.by_requestor_selector.insert((requestor, selector), entry("0.003 USD")); + + let result = overrides.resolve(&requestor, &selector); + assert_eq!(result.unwrap().to_string(), "0.003 USD"); + } + + #[test] + fn test_resolve_selector_over_requestor() { + let requestor = addr("0x0000000000000000000000000000000000000001"); + let selector = sel("0x12345678"); + + let mut overrides = PricingOverrides::default(); + overrides.by_requestor.insert(requestor, entry("0.001 USD")); + overrides.by_selector.insert(selector, entry("0.002 USD")); + + let result = overrides.resolve(&requestor, &selector); + assert_eq!(result.unwrap().to_string(), "0.002 USD"); + } + + #[test] + fn test_json_round_trip() { + let json = r#"{ + "by_requestor": { + "0x0000000000000000000000000000000000000001": { "min_mcycle_price": "0.001 USD" } + }, + "by_selector": { + "0x12345678": { "min_mcycle_price": "0.005 ETH" } + }, + "by_requestor_selector": { + "0x0000000000000000000000000000000000000001:0x12345678": { "min_mcycle_price": "0.01 USD" } + } + }"#; + + let overrides: PricingOverrides = serde_json::from_str(json).unwrap(); + + assert_eq!(overrides.by_requestor.len(), 1); + assert_eq!(overrides.by_selector.len(), 1); + assert_eq!(overrides.by_requestor_selector.len(), 1); + + let requestor = addr("0x0000000000000000000000000000000000000001"); + let selector = sel("0x12345678"); + + assert_eq!(overrides.resolve(&requestor, &selector).unwrap().to_string(), "0.01 USD"); + + let other_sel = sel("0xdeadbeef"); + assert_eq!(overrides.resolve(&requestor, &other_sel).unwrap().to_string(), "0.001 USD"); + + let other_addr = addr("0x0000000000000000000000000000000000000099"); + assert_eq!(overrides.resolve(&other_addr, &selector).unwrap().to_string(), "0.005 ETH"); + + assert!(overrides.resolve(&other_addr, &other_sel).is_none()); + } + + #[test] + fn test_json_serialize_deserialize_round_trip() { + let requestor = addr("0x0000000000000000000000000000000000000001"); + let selector = sel("0x12345678"); + + let mut overrides = PricingOverrides::default(); + overrides.by_requestor.insert(requestor, entry("0.001 USD")); + overrides.by_selector.insert(selector, entry("0.005 ETH")); + overrides.by_requestor_selector.insert((requestor, selector), entry("0.01 USD")); + + let json = serde_json::to_string(&overrides).unwrap(); + let deserialized: PricingOverrides = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.by_requestor.len(), 1); + assert_eq!(deserialized.by_selector.len(), 1); + assert_eq!(deserialized.by_requestor_selector.len(), 1); + assert_eq!(deserialized.resolve(&requestor, &selector).unwrap().to_string(), "0.01 USD"); + } + + #[test] + fn test_json_invalid_requestor_selector_key() { + let json = r#"{ + "by_requestor_selector": { + "bad-key-no-colon": { "min_mcycle_price": "0.001 USD" } + } + }"#; + let result: Result = serde_json::from_str(json); + assert!(result.is_err()); + } + + #[test] + fn test_json_empty_maps() { + let json = r#"{}"#; + let overrides: PricingOverrides = serde_json::from_str(json).unwrap(); + assert!(overrides.by_requestor.is_empty()); + assert!(overrides.by_selector.is_empty()); + assert!(overrides.by_requestor_selector.is_empty()); + } + + #[test] + fn test_market_config_default_has_no_overrides_path() { + let config = MarketConfig::default(); + assert!(config.pricing_overrides_path.is_none()); + } +} diff --git a/crates/boundless-market/src/prover_utils/mod.rs b/crates/boundless-market/src/prover_utils/mod.rs index 23c08c35b..7314e8074 100644 --- a/crates/boundless-market/src/prover_utils/mod.rs +++ b/crates/boundless-market/src/prover_utils/mod.rs @@ -23,7 +23,7 @@ pub use config::MarketConfig; #[cfg(feature = "prover_utils")] pub use config::{ defaults as config_defaults, BatcherConfig, Config, MarketConfig, OrderCommitmentPriority, - OrderPricingPriority, ProverConfig, + OrderPricingPriority, PricingOverrideEntry, PricingOverrides, ProverConfig, }; use crate::{ @@ -422,6 +422,15 @@ pub trait OrderPricingContext { /// Convert an Amount to ZKC using the price oracle. async fn convert_to_zkc(&self, amount: &Amount) -> Result; + /// Resolve the effective `min_mcycle_price` for an order. + /// + /// The default implementation returns `MarketConfig::min_mcycle_price`. + /// Override this to implement per-requestor / per-selector pricing (see [`config::PricingOverrides`]). + fn resolve_min_mcycle_price(&self, order: &OrderRequest) -> Result { + let _ = order; + Ok(self.market_config()?.min_mcycle_price.clone()) + } + /// Access to the prover for preflight operations. fn prover(&self) -> &ProverObj; @@ -931,8 +940,10 @@ pub trait OrderPricingContext { config_min_mcycle_price: config_min_mcycle_price_collateral_tokens, }) } else { - // For lockable orders, evaluate based on ETH price - let config_min_mcycle_price_amount = &config.min_mcycle_price; + // For lockable orders, evaluate based on ETH price. + // Use per-requestor/selector override if configured, otherwise global default. + let resolved_min_mcycle_price = self.resolve_min_mcycle_price(order)?; + let config_min_mcycle_price_amount = &resolved_min_mcycle_price; // Convert configured price to ETH (i.e., handles USD via price oracle) let config_min_mcycle_price_eth = @@ -1045,7 +1056,9 @@ pub trait OrderPricingContext { let lock_expiry = order.request.lock_expires_at(); let order_expiry = order.request.expires_at(); let config = self.market_config()?; - let min_mcycle_price_amount = &config.min_mcycle_price; + // Use per-requestor/selector override if configured, otherwise global default. + let resolved_min_mcycle_price = self.resolve_min_mcycle_price(order)?; + let min_mcycle_price_amount = &resolved_min_mcycle_price; // Convert configured price to ETH (handles USD via price oracle) let min_mcycle_price_eth = self.convert_to_eth(min_mcycle_price_amount).await?; diff --git a/crates/broker/src/lib.rs b/crates/broker/src/lib.rs index 061063a3d..954d96311 100644 --- a/crates/broker/src/lib.rs +++ b/crates/broker/src/lib.rs @@ -883,6 +883,41 @@ where Ok(()) }); + // Load pricing overrides if configured, with hot-reload support + let pricing_overrides_path = config + .lock_all() + .context("Failed to read config for pricing overrides")? + .market + .pricing_overrides_path + .clone(); + let pricing_overrides = Arc::new(std::sync::RwLock::new(None)); + if let Some(ref path) = pricing_overrides_path { + match boundless_market::prover_utils::PricingOverrides::load(path).await { + Ok(overrides) => { + log_pricing_overrides(path, &overrides); + *pricing_overrides.write().expect("pricing overrides lock poisoned") = + Some(overrides); + } + Err(e) + if e.downcast_ref::() + .is_some_and(|io| io.kind() == std::io::ErrorKind::NotFound) => + { + tracing::info!( + "Pricing overrides file {} not found yet, will pick it up when created", + path.display() + ); + } + Err(e) => return Err(e.context("Failed to load pricing overrides")), + } + let reload_overrides = pricing_overrides.clone(); + let reload_path = path.clone(); + let cancel_token = non_critical_cancel_token.clone(); + non_critical_tasks.spawn(async move { + pricing_overrides_reload_task(reload_path, reload_overrides, cancel_token).await; + Ok(()) + }); + } + // Spin up the order picker to pre-flight and find orders to lock let order_picker = Arc::new(order_picker::OrderPicker::new( self.db.clone(), @@ -899,6 +934,7 @@ where self.allow_requestors.clone(), self.downloader.clone(), price_oracle.clone(), + pricing_overrides, )); let cloned_config = config.clone(); let cancel_token = non_critical_cancel_token.clone(); @@ -1270,5 +1306,77 @@ pub mod test_utils { } } +const PRICING_OVERRIDES_RELOAD_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60); + +fn log_pricing_overrides( + path: &std::path::Path, + overrides: &boundless_market::prover_utils::PricingOverrides, +) { + tracing::info!( + "Loaded pricing overrides from {}: {} requestor, {} selector, {} requestor+selector entries", + path.display(), + overrides.by_requestor.len(), + overrides.by_selector.len(), + overrides.by_requestor_selector.len(), + ); +} + +async fn pricing_overrides_reload_task( + path: PathBuf, + overrides: Arc>>, + cancel_token: CancellationToken, +) { + let mut last_modified: Option = None; + + // Capture the initial mtime so the first poll only reloads on actual change. + if let Ok(meta) = tokio::fs::metadata(&path).await { + last_modified = meta.modified().ok(); + } + + loop { + tokio::select! { + _ = cancel_token.cancelled() => break, + _ = tokio::time::sleep(PRICING_OVERRIDES_RELOAD_INTERVAL) => {} + } + + match tokio::fs::metadata(&path).await { + Ok(meta) => { + let modified = meta.modified().ok(); + if modified == last_modified { + continue; + } + match boundless_market::prover_utils::PricingOverrides::load(&path).await { + Ok(new_overrides) => { + log_pricing_overrides(&path, &new_overrides); + *overrides.write().expect("pricing overrides lock poisoned") = + Some(new_overrides); + last_modified = modified; + } + Err(e) => { + tracing::warn!( + "Failed to reload pricing overrides from {}: {:#}", + path.display(), + e + ); + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + if last_modified.is_some() { + tracing::info!( + "Pricing overrides file {} removed, clearing overrides", + path.display() + ); + *overrides.write().expect("pricing overrides lock poisoned") = None; + last_modified = None; + } + } + Err(e) => { + tracing::warn!("Failed to check pricing overrides file {}: {}", path.display(), e); + } + } + } +} + #[cfg(test)] pub mod tests; diff --git a/crates/broker/src/order_picker.rs b/crates/broker/src/order_picker.rs index fc4d440d1..5d8ea8a81 100644 --- a/crates/broker/src/order_picker.rs +++ b/crates/broker/src/order_picker.rs @@ -94,6 +94,8 @@ pub struct OrderPicker

{ allow_requestors: AllowRequestors, downloader: ConfigurableDownloader, price_oracle: Arc, + pricing_overrides: + Arc>>, } impl

OrderPicker

@@ -116,6 +118,9 @@ where allow_requestors: AllowRequestors, downloader: ConfigurableDownloader, price_oracle: Arc, + pricing_overrides: Arc< + std::sync::RwLock>, + >, ) -> Self { let market = BoundlessMarketService::new_for_broker( market_addr, @@ -153,6 +158,7 @@ where allow_requestors, downloader, price_oracle, + pricing_overrides, } } @@ -279,6 +285,27 @@ where Ok(config.market.deny_requestor_addresses.clone()) } + fn resolve_min_mcycle_price(&self, order: &OrderRequest) -> Result { + let config = self.market_config()?; + let guard = self.pricing_overrides.read().expect("pricing overrides lock poisoned"); + if let Some(overrides) = guard.as_ref() { + let requestor = order.request.client_address(); + let selector = order.request.requirements.selector; + if let Some(amount) = overrides.resolve(&requestor, &selector) { + tracing::debug!( + order_id = %order.id(), + %requestor, + %selector, + override_price = %amount, + global_price = %config.min_mcycle_price, + "Using pricing override instead of global min_mcycle_price" + ); + return Ok(amount.clone()); + } + } + Ok(config.min_mcycle_price.clone()) + } + fn supported_selectors(&self) -> &SupportedSelectors { &self.supported_selectors } @@ -960,6 +987,7 @@ pub(crate) mod tests { config: Option, collateral_token_decimals: Option, prover: Option, + pricing_overrides: Option, } impl PickerTestCtxBuilder { @@ -979,6 +1007,12 @@ pub(crate) mod tests { pub(crate) fn with_collateral_token_decimals(self, decimals: u8) -> Self { Self { collateral_token_decimals: Some(decimals), ..self } } + pub(crate) fn with_pricing_overrides( + self, + overrides: boundless_market::prover_utils::PricingOverrides, + ) -> Self { + Self { pricing_overrides: Some(overrides), ..self } + } pub(crate) async fn build( self, ) -> PickerTestCtx { @@ -1060,6 +1094,7 @@ pub(crate) mod tests { allow_requestors, downloader, create_test_price_oracle(), + Arc::new(std::sync::RwLock::new(self.pricing_overrides)), ); PickerTestCtx { @@ -2922,4 +2957,155 @@ pub(crate) mod tests { let priced_order = ctx.priced_orders_rx.try_recv().unwrap(); assert!(priced_order.target_timestamp.is_some()); } + + #[tokio::test] + async fn test_calculate_exec_limits_with_requestor_pricing_override() { + let global_price = "0.001 ETH"; + let override_price = "0.01 ETH"; + + let mut market_config = MarketConfig::default(); + market_config.min_mcycle_price = Amount::parse(global_price, None).unwrap(); + market_config.min_mcycle_price_collateral_token = Amount::parse("10 ZKC", None).unwrap(); + market_config.max_mcycle_limit = 100_000; + + let config = ConfigLock::default(); + config.load_write().unwrap().market = market_config; + + // Build without overrides first to get the baseline + let ctx_no_override = + PickerTestCtxBuilder::default().with_config(config.clone()).build().await; + + let gas_cost = parse_ether("0.001").unwrap(); + let order = ctx_no_override + .generate_next_order(OrderParams { + fulfillment_type: FulfillmentType::LockAndFulfill, + max_price: parse_ether("0.05").unwrap(), + lock_collateral: parse_collateral_tokens("100"), + lock_timeout: 900, + timeout: 1200, + ..Default::default() + }) + .await; + + let requestor_addr = order.request.client_address(); + + let (_, baseline_prove_limit, _) = + ctx_no_override.picker.calculate_exec_limits(&order, gas_cost).await.unwrap(); + + // ETH based: (0.05 - 0.001) * 1M / 0.001 = 49M cycles + assert_eq!(baseline_prove_limit, 49_000_000u64); + + // Now build with a requestor override: 10x the global min_mcycle_price + let mut overrides = boundless_market::prover_utils::PricingOverrides::default(); + overrides.by_requestor.insert( + requestor_addr, + boundless_market::prover_utils::PricingOverrideEntry { + min_mcycle_price: Amount::parse(override_price, None).unwrap(), + }, + ); + + let config2 = ConfigLock::default(); + config2.load_write().unwrap().market = config.lock_all().unwrap().market.clone(); + + let ctx_with_override = PickerTestCtxBuilder::default() + .with_config(config2) + .with_pricing_overrides(overrides) + .build() + .await; + + let order2 = ctx_with_override + .generate_next_order(OrderParams { + fulfillment_type: FulfillmentType::LockAndFulfill, + max_price: parse_ether("0.05").unwrap(), + lock_collateral: parse_collateral_tokens("100"), + lock_timeout: 900, + timeout: 1200, + ..Default::default() + }) + .await; + + let (_, overridden_prove_limit, _) = + ctx_with_override.picker.calculate_exec_limits(&order2, gas_cost).await.unwrap(); + + // ETH based with override: (0.05 - 0.001) * 1M / 0.01 = 4.9M cycles + assert_eq!(overridden_prove_limit, 4_900_000u64); + + // The override should produce a stricter (lower) limit + assert!( + overridden_prove_limit < baseline_prove_limit, + "Override price {override_price} should produce a lower exec limit than global {global_price}: \ + got {overridden_prove_limit} vs {baseline_prove_limit}" + ); + } + + #[tokio::test] + async fn test_calculate_exec_limits_selector_override_takes_priority() { + let mut market_config = MarketConfig::default(); + market_config.min_mcycle_price = Amount::parse("0.001 ETH", None).unwrap(); + market_config.min_mcycle_price_collateral_token = Amount::parse("10 ZKC", None).unwrap(); + market_config.max_mcycle_limit = 100_000; + + let config = ConfigLock::default(); + config.load_write().unwrap().market = market_config; + + // Set up overrides: requestor gets 0.005 ETH, but selector gets 0.01 ETH. + // Selector should win per the cascade priority. + let ctx_baseline = + PickerTestCtxBuilder::default().with_config(config.clone()).build().await; + + let order_template = ctx_baseline + .generate_next_order(OrderParams { + fulfillment_type: FulfillmentType::LockAndFulfill, + max_price: parse_ether("0.05").unwrap(), + lock_collateral: parse_collateral_tokens("100"), + lock_timeout: 900, + timeout: 1200, + ..Default::default() + }) + .await; + + let requestor_addr = order_template.request.client_address(); + let selector = order_template.request.requirements.selector; + + let mut overrides = boundless_market::prover_utils::PricingOverrides::default(); + overrides.by_requestor.insert( + requestor_addr, + boundless_market::prover_utils::PricingOverrideEntry { + min_mcycle_price: Amount::parse("0.005 ETH", None).unwrap(), + }, + ); + overrides.by_selector.insert( + selector, + boundless_market::prover_utils::PricingOverrideEntry { + min_mcycle_price: Amount::parse("0.01 ETH", None).unwrap(), + }, + ); + + let config2 = ConfigLock::default(); + config2.load_write().unwrap().market = config.lock_all().unwrap().market.clone(); + + let ctx = PickerTestCtxBuilder::default() + .with_config(config2) + .with_pricing_overrides(overrides) + .build() + .await; + + let gas_cost = parse_ether("0.001").unwrap(); + let order = ctx + .generate_next_order(OrderParams { + fulfillment_type: FulfillmentType::LockAndFulfill, + max_price: parse_ether("0.05").unwrap(), + lock_collateral: parse_collateral_tokens("100"), + lock_timeout: 900, + timeout: 1200, + ..Default::default() + }) + .await; + + let (_, prove_limit, _) = ctx.picker.calculate_exec_limits(&order, gas_cost).await.unwrap(); + + // Selector override (0.01 ETH) should take priority over requestor (0.005 ETH). + // ETH based: (0.05 - 0.001) * 1M / 0.01 = 4.9M cycles + assert_eq!(prove_limit, 4_900_000u64); + } } diff --git a/justfile b/justfile index e091be1e2..59c574080 100644 --- a/justfile +++ b/justfile @@ -392,6 +392,9 @@ localnet action="up": check-deps cp broker-template.toml broker.toml || { echo "Error: broker-template.toml not found"; exit 1; } echo "broker.toml created successfully." fi + if [ ! -f pricing-overrides.json ]; then + cp pricing-overrides.template.json pricing-overrides.json 2>/dev/null || true + fi echo "Make sure to run 'source .env.localnet' to load the environment variables before interacting with the network." echo "To start the broker manually, run:" echo "source .env.localnet && cp broker-template.toml broker.toml && cargo run --bin broker" @@ -522,6 +525,9 @@ prover action="up" env_file="" detached="true": cp broker-template.toml broker.toml || { echo "Error: broker-template.toml not found"; exit 1; } echo "broker.toml created successfully." fi + if [ ! -f pricing-overrides.json ]; then + cp pricing-overrides.template.json pricing-overrides.json 2>/dev/null || true + fi if [ "{{action}}" = "logs" ]; then # Ignore mining process logs by default diff --git a/pricing-overrides.template.json b/pricing-overrides.template.json new file mode 100644 index 000000000..3e39d0a5d --- /dev/null +++ b/pricing-overrides.template.json @@ -0,0 +1,5 @@ +{ + "by_requestor": {}, + "by_selector": {}, + "by_requestor_selector": {} +} From d7d8cb6b01200daf1d9d4d23f0a6f576292c133c Mon Sep 17 00:00:00 2001 From: capossele Date: Fri, 20 Feb 2026 12:35:08 +0000 Subject: [PATCH 2/9] simplify --- .gitignore | 1 - ansible/roles/prover/defaults/main.yml | 2 - ansible/roles/prover/tasks/main.yml | 9 -- broker-template.toml | 36 +++--- compose.yml | 3 - .../src/commands/prover/generate_config.rs | 12 -- .../src/prover_utils/config.rs | 27 ++--- crates/broker/src/lib.rs | 108 ------------------ crates/broker/src/order_picker.rs | 69 ++++------- justfile | 6 - pricing-overrides.template.json | 5 - 11 files changed, 47 insertions(+), 231 deletions(-) delete mode 100644 pricing-overrides.template.json diff --git a/.gitignore b/.gitignore index 4af614919..73abe0014 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ deployment_secrets.toml # Ignore broker config file(s) *broker*.toml -pricing-overrides.json ansible/inventory.yml # Cargo diff --git a/ansible/roles/prover/defaults/main.yml b/ansible/roles/prover/defaults/main.yml index 13f042c54..401964b27 100644 --- a/ansible/roles/prover/defaults/main.yml +++ b/ansible/roles/prover/defaults/main.yml @@ -64,7 +64,5 @@ prover_povw_log_id: "" # Broker configuration URL prover_broker_toml_url: "https://raw.githubusercontent.com/boundless-xyz/boundless/refs/heads/main/broker-template.toml" -# Pricing overrides JSON (default ships with empty overrides; edit on host to take effect within 60s) -prover_pricing_overrides_url: "https://raw.githubusercontent.com/boundless-xyz/boundless/refs/heads/main/pricing-overrides.template.json" prover_docker_compose_profile: "--profile broker --profile miner" prover_docker_compose_invoke: "" diff --git a/ansible/roles/prover/tasks/main.yml b/ansible/roles/prover/tasks/main.yml index 52878a102..df096d630 100644 --- a/ansible/roles/prover/tasks/main.yml +++ b/ansible/roles/prover/tasks/main.yml @@ -27,15 +27,6 @@ mode: '0644' notify: Restart Bento -- name: Download pricing overrides file (creates default if missing) - ansible.builtin.get_url: - url: '{{ prover_pricing_overrides_url }}' - dest: "{{ prover_dir }}/pricing-overrides.json" - owner: root - group: root - mode: '0644' - force: false - - name: Write the system service file ansible.builtin.template: src: bento.service.j2 diff --git a/broker-template.toml b/broker-template.toml index 0b7c85937..59a1aaa26 100644 --- a/broker-template.toml +++ b/broker-template.toml @@ -25,26 +25,22 @@ # `min_mcycle_price` to determine if the order should be locked. min_mcycle_price = "0.00002 USD" -# Optional path to a JSON file with per-requestor and per-selector pricing overrides. -# When set, the broker checks the file for a matching override before falling back to -# the global min_mcycle_price above. See the example file format: -# -# { -# "by_requestor": { -# "0xAbC...123": { "min_mcycle_price": "0.0001 USD" } -# }, -# "by_selector": { -# "0x12345678": { "min_mcycle_price": "0.0005 USD" } -# }, -# "by_requestor_selector": { -# "0xAbC...123:0x12345678": { "min_mcycle_price": "0.001 USD" } -# } -# } -# -# Resolution priority: requestor+selector > selector > requestor > global default. -# The file is automatically hot-reloaded every 60 seconds; no broker restart needed. -# Copy pricing-overrides.template.json to pricing-overrides.json to get started. -pricing_overrides_path = "pricing-overrides.json" + +# Per-requestor and per-selector min_mcycle_price overrides. +# +# Resolution priority: by_requestor_selector > by_selector > by_requestor > global default. +# Changes take effect immediately when the broker reloads this file. +# +# Uncomment and populate any of the sections below to enable overrides: +# +# [market.pricing_overrides.by_requestor] +# "0xAbC...123" = { min_mcycle_price = "0.0001 USD" } +# +# [market.pricing_overrides.by_selector] +# "0x12345678" = { min_mcycle_price = "0.0005 USD" } +# +# [market.pricing_overrides.by_requestor_selector] +# "0xAbC...123:0x12345678" = { min_mcycle_price = "0.001 USD" } # The minimum price per mega-cycle (i.e. million RISC-V cycles) for the broker # to attempt to fulfill an order in the case that the order was locked by another prover diff --git a/compose.yml b/compose.yml index 3e097e850..f5b5c3c75 100644 --- a/compose.yml +++ b/compose.yml @@ -283,9 +283,6 @@ services: - type: bind source: ./broker.toml target: /app/broker.toml - - type: bind - source: ./pricing-overrides.json - target: /app/pricing-overrides.json - broker-data:/db/ # Uncomment when using locally built set-builder and assessor guest programs # - type: bind diff --git a/crates/boundless-cli/src/commands/prover/generate_config.rs b/crates/boundless-cli/src/commands/prover/generate_config.rs index ba2922961..14f47b5dc 100644 --- a/crates/boundless-cli/src/commands/prover/generate_config.rs +++ b/crates/boundless-cli/src/commands/prover/generate_config.rs @@ -135,18 +135,6 @@ impl ProverGenerateConfig { self.generate_broker_toml(&config, broker_strategy, &display)?; display.item_colored("Created", self.broker_toml_file.display(), "green"); - // Create pricing-overrides.json next to broker.toml if it doesn't exist - let pricing_overrides_path = - self.broker_toml_file.parent().unwrap_or(Path::new(".")).join("pricing-overrides.json"); - if !pricing_overrides_path.exists() { - std::fs::write( - &pricing_overrides_path, - include_str!("../../../../../pricing-overrides.template.json"), - ) - .context("Failed to write pricing-overrides.json")?; - display.item_colored("Created", pricing_overrides_path.display(), "green"); - } - // Backup and generate compose.yml if let Some(backup_path) = self.backup_file(&self.compose_yml_file)? { display.item_colored("Backup saved", backup_path.display(), "cyan"); diff --git a/crates/boundless-market/src/prover_utils/config.rs b/crates/boundless-market/src/prover_utils/config.rs index 4985ee8d1..da0ba3d35 100644 --- a/crates/boundless-market/src/prover_utils/config.rs +++ b/crates/boundless-market/src/prover_utils/config.rs @@ -502,12 +502,13 @@ pub struct MarketConfig { /// market. This should remain false to avoid losing partial PoVW jobs. #[serde(default)] pub cancel_proving_expired_orders: bool, - /// Optional path to a JSON file containing per-requestor and per-selector pricing overrides. + /// Per-requestor and per-selector `min_mcycle_price` overrides. /// - /// The file is hot-reloaded automatically (every 60 s); no broker restart needed. - /// See [`PricingOverrides`] for the file format. + /// When the broker evaluates an order, it resolves the effective `min_mcycle_price` + /// with priority: `by_requestor_selector` > `by_selector` > `by_requestor` > global default. + /// Changes to this section are picked up automatically when the broker reloads `broker.toml`. #[serde(default)] - pub pricing_overrides_path: Option, + pub pricing_overrides: PricingOverrides, } impl Default for MarketConfig { @@ -554,7 +555,7 @@ impl Default for MarketConfig { order_pricing_priority: OrderPricingPriority::default(), order_commitment_priority: OrderCommitmentPriority::default(), cancel_proving_expired_orders: false, - pricing_overrides_path: None, + pricing_overrides: PricingOverrides::default(), } } } @@ -802,16 +803,6 @@ where } impl PricingOverrides { - /// Load overrides from a JSON file. - #[cfg(any(feature = "prover_utils", feature = "test-utils"))] - pub async fn load(path: &Path) -> Result { - let data = fs::read_to_string(path) - .await - .context(format!("Failed to read pricing overrides from {path:?}"))?; - serde_json::from_str(&data) - .context(format!("Failed to parse pricing overrides from {path:?}")) - } - /// Resolve the effective `min_mcycle_price` for a given requestor and selector. /// /// Returns `Some(amount)` if an override matches, `None` to use the global default. @@ -984,8 +975,10 @@ mod tests { } #[test] - fn test_market_config_default_has_no_overrides_path() { + fn test_market_config_default_has_empty_pricing_overrides() { let config = MarketConfig::default(); - assert!(config.pricing_overrides_path.is_none()); + assert!(config.pricing_overrides.by_requestor.is_empty()); + assert!(config.pricing_overrides.by_selector.is_empty()); + assert!(config.pricing_overrides.by_requestor_selector.is_empty()); } } diff --git a/crates/broker/src/lib.rs b/crates/broker/src/lib.rs index 954d96311..061063a3d 100644 --- a/crates/broker/src/lib.rs +++ b/crates/broker/src/lib.rs @@ -883,41 +883,6 @@ where Ok(()) }); - // Load pricing overrides if configured, with hot-reload support - let pricing_overrides_path = config - .lock_all() - .context("Failed to read config for pricing overrides")? - .market - .pricing_overrides_path - .clone(); - let pricing_overrides = Arc::new(std::sync::RwLock::new(None)); - if let Some(ref path) = pricing_overrides_path { - match boundless_market::prover_utils::PricingOverrides::load(path).await { - Ok(overrides) => { - log_pricing_overrides(path, &overrides); - *pricing_overrides.write().expect("pricing overrides lock poisoned") = - Some(overrides); - } - Err(e) - if e.downcast_ref::() - .is_some_and(|io| io.kind() == std::io::ErrorKind::NotFound) => - { - tracing::info!( - "Pricing overrides file {} not found yet, will pick it up when created", - path.display() - ); - } - Err(e) => return Err(e.context("Failed to load pricing overrides")), - } - let reload_overrides = pricing_overrides.clone(); - let reload_path = path.clone(); - let cancel_token = non_critical_cancel_token.clone(); - non_critical_tasks.spawn(async move { - pricing_overrides_reload_task(reload_path, reload_overrides, cancel_token).await; - Ok(()) - }); - } - // Spin up the order picker to pre-flight and find orders to lock let order_picker = Arc::new(order_picker::OrderPicker::new( self.db.clone(), @@ -934,7 +899,6 @@ where self.allow_requestors.clone(), self.downloader.clone(), price_oracle.clone(), - pricing_overrides, )); let cloned_config = config.clone(); let cancel_token = non_critical_cancel_token.clone(); @@ -1306,77 +1270,5 @@ pub mod test_utils { } } -const PRICING_OVERRIDES_RELOAD_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60); - -fn log_pricing_overrides( - path: &std::path::Path, - overrides: &boundless_market::prover_utils::PricingOverrides, -) { - tracing::info!( - "Loaded pricing overrides from {}: {} requestor, {} selector, {} requestor+selector entries", - path.display(), - overrides.by_requestor.len(), - overrides.by_selector.len(), - overrides.by_requestor_selector.len(), - ); -} - -async fn pricing_overrides_reload_task( - path: PathBuf, - overrides: Arc>>, - cancel_token: CancellationToken, -) { - let mut last_modified: Option = None; - - // Capture the initial mtime so the first poll only reloads on actual change. - if let Ok(meta) = tokio::fs::metadata(&path).await { - last_modified = meta.modified().ok(); - } - - loop { - tokio::select! { - _ = cancel_token.cancelled() => break, - _ = tokio::time::sleep(PRICING_OVERRIDES_RELOAD_INTERVAL) => {} - } - - match tokio::fs::metadata(&path).await { - Ok(meta) => { - let modified = meta.modified().ok(); - if modified == last_modified { - continue; - } - match boundless_market::prover_utils::PricingOverrides::load(&path).await { - Ok(new_overrides) => { - log_pricing_overrides(&path, &new_overrides); - *overrides.write().expect("pricing overrides lock poisoned") = - Some(new_overrides); - last_modified = modified; - } - Err(e) => { - tracing::warn!( - "Failed to reload pricing overrides from {}: {:#}", - path.display(), - e - ); - } - } - } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - if last_modified.is_some() { - tracing::info!( - "Pricing overrides file {} removed, clearing overrides", - path.display() - ); - *overrides.write().expect("pricing overrides lock poisoned") = None; - last_modified = None; - } - } - Err(e) => { - tracing::warn!("Failed to check pricing overrides file {}: {}", path.display(), e); - } - } - } -} - #[cfg(test)] pub mod tests; diff --git a/crates/broker/src/order_picker.rs b/crates/broker/src/order_picker.rs index 5d8ea8a81..f5de3896b 100644 --- a/crates/broker/src/order_picker.rs +++ b/crates/broker/src/order_picker.rs @@ -94,8 +94,6 @@ pub struct OrderPicker

{ allow_requestors: AllowRequestors, downloader: ConfigurableDownloader, price_oracle: Arc, - pricing_overrides: - Arc>>, } impl

OrderPicker

@@ -118,9 +116,6 @@ where allow_requestors: AllowRequestors, downloader: ConfigurableDownloader, price_oracle: Arc, - pricing_overrides: Arc< - std::sync::RwLock>, - >, ) -> Self { let market = BoundlessMarketService::new_for_broker( market_addr, @@ -158,7 +153,6 @@ where allow_requestors, downloader, price_oracle, - pricing_overrides, } } @@ -287,21 +281,18 @@ where fn resolve_min_mcycle_price(&self, order: &OrderRequest) -> Result { let config = self.market_config()?; - let guard = self.pricing_overrides.read().expect("pricing overrides lock poisoned"); - if let Some(overrides) = guard.as_ref() { - let requestor = order.request.client_address(); - let selector = order.request.requirements.selector; - if let Some(amount) = overrides.resolve(&requestor, &selector) { - tracing::debug!( - order_id = %order.id(), - %requestor, - %selector, - override_price = %amount, - global_price = %config.min_mcycle_price, - "Using pricing override instead of global min_mcycle_price" - ); - return Ok(amount.clone()); - } + let requestor = order.request.client_address(); + let selector = order.request.requirements.selector; + if let Some(amount) = config.pricing_overrides.resolve(&requestor, &selector) { + tracing::debug!( + order_id = %order.id(), + %requestor, + %selector, + override_price = %amount, + global_price = %config.min_mcycle_price, + "Using pricing override instead of global min_mcycle_price" + ); + return Ok(amount.clone()); } Ok(config.min_mcycle_price.clone()) } @@ -987,7 +978,6 @@ pub(crate) mod tests { config: Option, collateral_token_decimals: Option, prover: Option, - pricing_overrides: Option, } impl PickerTestCtxBuilder { @@ -1007,12 +997,6 @@ pub(crate) mod tests { pub(crate) fn with_collateral_token_decimals(self, decimals: u8) -> Self { Self { collateral_token_decimals: Some(decimals), ..self } } - pub(crate) fn with_pricing_overrides( - self, - overrides: boundless_market::prover_utils::PricingOverrides, - ) -> Self { - Self { pricing_overrides: Some(overrides), ..self } - } pub(crate) async fn build( self, ) -> PickerTestCtx { @@ -1094,7 +1078,6 @@ pub(crate) mod tests { allow_requestors, downloader, create_test_price_oracle(), - Arc::new(std::sync::RwLock::new(self.pricing_overrides)), ); PickerTestCtx { @@ -2996,22 +2979,17 @@ pub(crate) mod tests { assert_eq!(baseline_prove_limit, 49_000_000u64); // Now build with a requestor override: 10x the global min_mcycle_price - let mut overrides = boundless_market::prover_utils::PricingOverrides::default(); - overrides.by_requestor.insert( + let mut config2_market = config.lock_all().unwrap().market.clone(); + config2_market.pricing_overrides.by_requestor.insert( requestor_addr, boundless_market::prover_utils::PricingOverrideEntry { min_mcycle_price: Amount::parse(override_price, None).unwrap(), }, ); - let config2 = ConfigLock::default(); - config2.load_write().unwrap().market = config.lock_all().unwrap().market.clone(); + config2.load_write().unwrap().market = config2_market; - let ctx_with_override = PickerTestCtxBuilder::default() - .with_config(config2) - .with_pricing_overrides(overrides) - .build() - .await; + let ctx_with_override = PickerTestCtxBuilder::default().with_config(config2).build().await; let order2 = ctx_with_override .generate_next_order(OrderParams { @@ -3067,28 +3045,23 @@ pub(crate) mod tests { let requestor_addr = order_template.request.client_address(); let selector = order_template.request.requirements.selector; - let mut overrides = boundless_market::prover_utils::PricingOverrides::default(); - overrides.by_requestor.insert( + let mut config2_market = config.lock_all().unwrap().market.clone(); + config2_market.pricing_overrides.by_requestor.insert( requestor_addr, boundless_market::prover_utils::PricingOverrideEntry { min_mcycle_price: Amount::parse("0.005 ETH", None).unwrap(), }, ); - overrides.by_selector.insert( + config2_market.pricing_overrides.by_selector.insert( selector, boundless_market::prover_utils::PricingOverrideEntry { min_mcycle_price: Amount::parse("0.01 ETH", None).unwrap(), }, ); - let config2 = ConfigLock::default(); - config2.load_write().unwrap().market = config.lock_all().unwrap().market.clone(); + config2.load_write().unwrap().market = config2_market; - let ctx = PickerTestCtxBuilder::default() - .with_config(config2) - .with_pricing_overrides(overrides) - .build() - .await; + let ctx = PickerTestCtxBuilder::default().with_config(config2).build().await; let gas_cost = parse_ether("0.001").unwrap(); let order = ctx diff --git a/justfile b/justfile index 59c574080..e091be1e2 100644 --- a/justfile +++ b/justfile @@ -392,9 +392,6 @@ localnet action="up": check-deps cp broker-template.toml broker.toml || { echo "Error: broker-template.toml not found"; exit 1; } echo "broker.toml created successfully." fi - if [ ! -f pricing-overrides.json ]; then - cp pricing-overrides.template.json pricing-overrides.json 2>/dev/null || true - fi echo "Make sure to run 'source .env.localnet' to load the environment variables before interacting with the network." echo "To start the broker manually, run:" echo "source .env.localnet && cp broker-template.toml broker.toml && cargo run --bin broker" @@ -525,9 +522,6 @@ prover action="up" env_file="" detached="true": cp broker-template.toml broker.toml || { echo "Error: broker-template.toml not found"; exit 1; } echo "broker.toml created successfully." fi - if [ ! -f pricing-overrides.json ]; then - cp pricing-overrides.template.json pricing-overrides.json 2>/dev/null || true - fi if [ "{{action}}" = "logs" ]; then # Ignore mining process logs by default diff --git a/pricing-overrides.template.json b/pricing-overrides.template.json deleted file mode 100644 index 3e39d0a5d..000000000 --- a/pricing-overrides.template.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "by_requestor": {}, - "by_selector": {}, - "by_requestor_selector": {} -} From 16df5db2ee430edd9ac074ff047a24025d7a0591 Mon Sep 17 00:00:00 2001 From: capossele Date: Fri, 20 Feb 2026 12:41:34 +0000 Subject: [PATCH 3/9] toml tests --- .../src/prover_utils/config.rs | 128 ++++++++++++------ 1 file changed, 88 insertions(+), 40 deletions(-) diff --git a/crates/boundless-market/src/prover_utils/config.rs b/crates/boundless-market/src/prover_utils/config.rs index da0ba3d35..5bd388c37 100644 --- a/crates/boundless-market/src/prover_utils/config.rs +++ b/crates/boundless-market/src/prover_utils/config.rs @@ -902,20 +902,18 @@ mod tests { } #[test] - fn test_json_round_trip() { - let json = r#"{ - "by_requestor": { - "0x0000000000000000000000000000000000000001": { "min_mcycle_price": "0.001 USD" } - }, - "by_selector": { - "0x12345678": { "min_mcycle_price": "0.005 ETH" } - }, - "by_requestor_selector": { - "0x0000000000000000000000000000000000000001:0x12345678": { "min_mcycle_price": "0.01 USD" } - } - }"#; - - let overrides: PricingOverrides = serde_json::from_str(json).unwrap(); + fn test_toml_round_trip() { + let toml = r#" +[by_requestor] +"0x0000000000000000000000000000000000000001" = { min_mcycle_price = "0.001 USD" } + +[by_selector] +"0x12345678" = { min_mcycle_price = "0.005 ETH" } + +[by_requestor_selector] +"0x0000000000000000000000000000000000000001:0x12345678" = { min_mcycle_price = "0.01 USD" } +"#; + let overrides: PricingOverrides = toml::from_str(toml).unwrap(); assert_eq!(overrides.by_requestor.len(), 1); assert_eq!(overrides.by_selector.len(), 1); @@ -936,44 +934,94 @@ mod tests { } #[test] - fn test_json_serialize_deserialize_round_trip() { - let requestor = addr("0x0000000000000000000000000000000000000001"); - let selector = sel("0x12345678"); - - let mut overrides = PricingOverrides::default(); - overrides.by_requestor.insert(requestor, entry("0.001 USD")); - overrides.by_selector.insert(selector, entry("0.005 ETH")); - overrides.by_requestor_selector.insert((requestor, selector), entry("0.01 USD")); - - let json = serde_json::to_string(&overrides).unwrap(); - let deserialized: PricingOverrides = serde_json::from_str(&json).unwrap(); - - assert_eq!(deserialized.by_requestor.len(), 1); - assert_eq!(deserialized.by_selector.len(), 1); - assert_eq!(deserialized.by_requestor_selector.len(), 1); - assert_eq!(deserialized.resolve(&requestor, &selector).unwrap().to_string(), "0.01 USD"); + fn test_toml_multiple_entries() { + let toml = r#" +[by_requestor] +"0x0000000000000000000000000000000000000001" = { min_mcycle_price = "0.001 USD" } +"0x0000000000000000000000000000000000000002" = { min_mcycle_price = "0.002 USD" } + +[by_selector] +"0x12345678" = { min_mcycle_price = "0.005 ETH" } +"0xdeadbeef" = { min_mcycle_price = "0.01 ETH" } + +[by_requestor_selector] +"0x0000000000000000000000000000000000000001:0x12345678" = { min_mcycle_price = "0.001 USD" } +"0x0000000000000000000000000000000000000002:0xdeadbeef" = { min_mcycle_price = "0.002 USD" } +"#; + let overrides: PricingOverrides = toml::from_str(toml).unwrap(); + + assert_eq!(overrides.by_requestor.len(), 2); + assert_eq!(overrides.by_selector.len(), 2); + assert_eq!(overrides.by_requestor_selector.len(), 2); + + // Verify serialize → deserialize round-trip preserves all entries + let serialized = toml::to_string(&overrides).unwrap(); + let deserialized: PricingOverrides = toml::from_str(&serialized).unwrap(); + assert_eq!(deserialized.by_requestor.len(), 2); + assert_eq!(deserialized.by_selector.len(), 2); + assert_eq!(deserialized.by_requestor_selector.len(), 2); } #[test] - fn test_json_invalid_requestor_selector_key() { - let json = r#"{ - "by_requestor_selector": { - "bad-key-no-colon": { "min_mcycle_price": "0.001 USD" } - } - }"#; - let result: Result = serde_json::from_str(json); + fn test_toml_invalid_requestor_selector_key() { + let toml = r#" +[by_requestor_selector] +"bad-key-no-colon" = { min_mcycle_price = "0.001 USD" } +"#; + let result: Result = toml::from_str(toml); assert!(result.is_err()); } #[test] - fn test_json_empty_maps() { - let json = r#"{}"#; - let overrides: PricingOverrides = serde_json::from_str(json).unwrap(); + fn test_toml_empty_overrides() { + let overrides: PricingOverrides = toml::from_str("").unwrap(); assert!(overrides.by_requestor.is_empty()); assert!(overrides.by_selector.is_empty()); assert!(overrides.by_requestor_selector.is_empty()); } + #[test] + fn test_market_config_toml_with_pricing_overrides() { + let toml = r#" +mcycle_price = "0.00002 USD" +mcycle_price_collateral_token = "0.001 ZKC" +max_stake = "10 USD" + +[pricing_overrides.by_requestor] +"0x0000000000000000000000000000000000000001" = { min_mcycle_price = "0.001 USD" } + +[pricing_overrides.by_selector] +"0x12345678" = { min_mcycle_price = "0.005 ETH" } + +[pricing_overrides.by_requestor_selector] +"0x0000000000000000000000000000000000000001:0x12345678" = { min_mcycle_price = "0.01 USD" } +"#; + let config: MarketConfig = toml::from_str(toml).unwrap(); + + let requestor = addr("0x0000000000000000000000000000000000000001"); + let selector = sel("0x12345678"); + + // requestor+selector combo takes highest priority + assert_eq!( + config.pricing_overrides.resolve(&requestor, &selector).unwrap().to_string(), + "0.01 USD" + ); + // requestor-only match + assert_eq!( + config.pricing_overrides.resolve(&requestor, &sel("0xdeadbeef")).unwrap().to_string(), + "0.001 USD" + ); + // selector-only match + assert_eq!( + config + .pricing_overrides + .resolve(&addr("0x0000000000000000000000000000000000000099"), &selector) + .unwrap() + .to_string(), + "0.005 ETH" + ); + } + #[test] fn test_market_config_default_has_empty_pricing_overrides() { let config = MarketConfig::default(); From 24687aa2453eb316ffae2f38c03359f6fcd9e221 Mon Sep 17 00:00:00 2001 From: capossele Date: Fri, 20 Feb 2026 12:50:01 +0000 Subject: [PATCH 4/9] simplify --- broker-template.toml | 6 +- .../src/prover_utils/config.rs | 100 +++++++----------- .../boundless-market/src/prover_utils/mod.rs | 2 +- crates/broker/src/order_picker.rs | 30 +++--- 4 files changed, 57 insertions(+), 81 deletions(-) diff --git a/broker-template.toml b/broker-template.toml index 59a1aaa26..cf11bef05 100644 --- a/broker-template.toml +++ b/broker-template.toml @@ -34,13 +34,13 @@ min_mcycle_price = "0.00002 USD" # Uncomment and populate any of the sections below to enable overrides: # # [market.pricing_overrides.by_requestor] -# "0xAbC...123" = { min_mcycle_price = "0.0001 USD" } +# "0xAbC...123" = "0.0001 USD" # # [market.pricing_overrides.by_selector] -# "0x12345678" = { min_mcycle_price = "0.0005 USD" } +# "0x12345678" = "0.0005 USD" # # [market.pricing_overrides.by_requestor_selector] -# "0xAbC...123:0x12345678" = { min_mcycle_price = "0.001 USD" } +# "0xAbC...123:0x12345678" = "0.001 USD" # The minimum price per mega-cycle (i.e. million RISC-V cycles) for the broker # to attempt to fulfill an order in the case that the order was locked by another prover diff --git a/crates/boundless-market/src/prover_utils/config.rs b/crates/boundless-market/src/prover_utils/config.rs index 5bd388c37..4bd653072 100644 --- a/crates/boundless-market/src/prover_utils/config.rs +++ b/crates/boundless-market/src/prover_utils/config.rs @@ -734,14 +734,7 @@ impl Config { } } -/// A single pricing override entry. -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct PricingOverrideEntry { - /// Minimum price per mega-cycle for this override. - pub min_mcycle_price: Amount, -} - -/// Per-requestor and per-selector pricing overrides loaded from a JSON file. +/// Per-requestor and per-selector `min_mcycle_price` overrides. /// /// Resolution priority (first match wins): /// 1. `by_requestor_selector` -- keyed by `"

:"` @@ -752,43 +745,42 @@ pub struct PricingOverrideEntry { pub struct PricingOverrides { /// Overrides keyed by requestor address (checksummed or lowercase hex). #[serde(default)] - pub by_requestor: HashMap, + pub by_requestor: HashMap, /// Overrides keyed by selector hex (e.g. `"0x12345678"`). #[serde(default)] - pub by_selector: HashMap, PricingOverrideEntry>, + pub by_selector: HashMap, Amount>, /// Overrides keyed by `":"` for the most specific match. #[serde( default, deserialize_with = "deserialize_requestor_selector_map", serialize_with = "serialize_requestor_selector_map" )] - pub by_requestor_selector: HashMap<(Address, FixedBytes<4>), PricingOverrideEntry>, + pub by_requestor_selector: HashMap<(Address, FixedBytes<4>), Amount>, } fn deserialize_requestor_selector_map<'de, D>( deserializer: D, -) -> Result), PricingOverrideEntry>, D::Error> +) -> Result), Amount>, D::Error> where D: serde::Deserializer<'de>, { - let raw: HashMap = HashMap::deserialize(deserializer)?; + let raw: HashMap = HashMap::deserialize(deserializer)?; let mut map = HashMap::with_capacity(raw.len()); - for (key, entry) in raw { - let parts: Vec<&str> = key.split(':').collect(); - if parts.len() != 2 { - return Err(serde::de::Error::custom(format!( + for (key, amount) in raw { + let (addr_str, sel_str) = key.split_once(':').ok_or_else(|| { + serde::de::Error::custom(format!( "by_requestor_selector key must be '
:', got '{key}'" - ))); - } - let addr: Address = parts[0].parse().map_err(serde::de::Error::custom)?; - let sel: FixedBytes<4> = parts[1].parse().map_err(serde::de::Error::custom)?; - map.insert((addr, sel), entry); + )) + })?; + let addr: Address = addr_str.parse().map_err(serde::de::Error::custom)?; + let sel: FixedBytes<4> = sel_str.parse().map_err(serde::de::Error::custom)?; + map.insert((addr, sel), amount); } Ok(map) } fn serialize_requestor_selector_map( - map: &HashMap<(Address, FixedBytes<4>), PricingOverrideEntry>, + map: &HashMap<(Address, FixedBytes<4>), Amount>, serializer: S, ) -> Result where @@ -796,8 +788,8 @@ where { use serde::ser::SerializeMap; let mut ser_map = serializer.serialize_map(Some(map.len()))?; - for ((addr, sel), entry) in map { - ser_map.serialize_entry(&format!("{addr}:{sel}"), entry)?; + for ((addr, sel), amount) in map { + ser_map.serialize_entry(&format!("{addr}:{sel}"), amount)?; } ser_map.end() } @@ -807,16 +799,10 @@ impl PricingOverrides { /// /// Returns `Some(amount)` if an override matches, `None` to use the global default. pub fn resolve(&self, requestor: &Address, selector: &FixedBytes<4>) -> Option<&Amount> { - if let Some(entry) = self.by_requestor_selector.get(&(*requestor, *selector)) { - return Some(&entry.min_mcycle_price); - } - if let Some(entry) = self.by_selector.get(selector) { - return Some(&entry.min_mcycle_price); - } - if let Some(entry) = self.by_requestor.get(requestor) { - return Some(&entry.min_mcycle_price); - } - None + self.by_requestor_selector + .get(&(*requestor, *selector)) + .or_else(|| self.by_selector.get(selector)) + .or_else(|| self.by_requestor.get(requestor)) } } @@ -836,10 +822,6 @@ mod tests { Amount::parse(s, None).unwrap() } - fn entry(s: &str) -> PricingOverrideEntry { - PricingOverrideEntry { min_mcycle_price: amount(s) } - } - #[test] fn test_resolve_empty_returns_none() { let overrides = PricingOverrides::default(); @@ -852,7 +834,7 @@ mod tests { fn test_resolve_by_requestor() { let requestor = addr("0x0000000000000000000000000000000000000001"); let mut overrides = PricingOverrides::default(); - overrides.by_requestor.insert(requestor, entry("0.001 USD")); + overrides.by_requestor.insert(requestor, amount("0.001 USD")); let result = overrides.resolve(&requestor, &sel("0xaabbccdd")); assert_eq!(result.unwrap().to_string(), "0.001 USD"); @@ -865,7 +847,7 @@ mod tests { fn test_resolve_by_selector() { let selector = sel("0x12345678"); let mut overrides = PricingOverrides::default(); - overrides.by_selector.insert(selector, entry("0.005 USD")); + overrides.by_selector.insert(selector, amount("0.005 USD")); let any_addr = addr("0x0000000000000000000000000000000000000099"); let result = overrides.resolve(&any_addr, &selector); @@ -880,9 +862,9 @@ mod tests { let selector = sel("0x12345678"); let mut overrides = PricingOverrides::default(); - overrides.by_requestor.insert(requestor, entry("0.001 USD")); - overrides.by_selector.insert(selector, entry("0.002 USD")); - overrides.by_requestor_selector.insert((requestor, selector), entry("0.003 USD")); + overrides.by_requestor.insert(requestor, amount("0.001 USD")); + overrides.by_selector.insert(selector, amount("0.002 USD")); + overrides.by_requestor_selector.insert((requestor, selector), amount("0.003 USD")); let result = overrides.resolve(&requestor, &selector); assert_eq!(result.unwrap().to_string(), "0.003 USD"); @@ -894,8 +876,8 @@ mod tests { let selector = sel("0x12345678"); let mut overrides = PricingOverrides::default(); - overrides.by_requestor.insert(requestor, entry("0.001 USD")); - overrides.by_selector.insert(selector, entry("0.002 USD")); + overrides.by_requestor.insert(requestor, amount("0.001 USD")); + overrides.by_selector.insert(selector, amount("0.002 USD")); let result = overrides.resolve(&requestor, &selector); assert_eq!(result.unwrap().to_string(), "0.002 USD"); @@ -905,13 +887,13 @@ mod tests { fn test_toml_round_trip() { let toml = r#" [by_requestor] -"0x0000000000000000000000000000000000000001" = { min_mcycle_price = "0.001 USD" } +"0x0000000000000000000000000000000000000001" = "0.001 USD" [by_selector] -"0x12345678" = { min_mcycle_price = "0.005 ETH" } +"0x12345678" = "0.005 ETH" [by_requestor_selector] -"0x0000000000000000000000000000000000000001:0x12345678" = { min_mcycle_price = "0.01 USD" } +"0x0000000000000000000000000000000000000001:0x12345678" = "0.01 USD" "#; let overrides: PricingOverrides = toml::from_str(toml).unwrap(); @@ -937,16 +919,16 @@ mod tests { fn test_toml_multiple_entries() { let toml = r#" [by_requestor] -"0x0000000000000000000000000000000000000001" = { min_mcycle_price = "0.001 USD" } -"0x0000000000000000000000000000000000000002" = { min_mcycle_price = "0.002 USD" } +"0x0000000000000000000000000000000000000001" = "0.001 USD" +"0x0000000000000000000000000000000000000002" = "0.002 USD" [by_selector] -"0x12345678" = { min_mcycle_price = "0.005 ETH" } -"0xdeadbeef" = { min_mcycle_price = "0.01 ETH" } +"0x12345678" = "0.005 ETH" +"0xdeadbeef" = "0.01 ETH" [by_requestor_selector] -"0x0000000000000000000000000000000000000001:0x12345678" = { min_mcycle_price = "0.001 USD" } -"0x0000000000000000000000000000000000000002:0xdeadbeef" = { min_mcycle_price = "0.002 USD" } +"0x0000000000000000000000000000000000000001:0x12345678" = "0.001 USD" +"0x0000000000000000000000000000000000000002:0xdeadbeef" = "0.002 USD" "#; let overrides: PricingOverrides = toml::from_str(toml).unwrap(); @@ -966,7 +948,7 @@ mod tests { fn test_toml_invalid_requestor_selector_key() { let toml = r#" [by_requestor_selector] -"bad-key-no-colon" = { min_mcycle_price = "0.001 USD" } +"bad-key-no-colon" = "0.001 USD" "#; let result: Result = toml::from_str(toml); assert!(result.is_err()); @@ -988,13 +970,13 @@ mcycle_price_collateral_token = "0.001 ZKC" max_stake = "10 USD" [pricing_overrides.by_requestor] -"0x0000000000000000000000000000000000000001" = { min_mcycle_price = "0.001 USD" } +"0x0000000000000000000000000000000000000001" = "0.001 USD" [pricing_overrides.by_selector] -"0x12345678" = { min_mcycle_price = "0.005 ETH" } +"0x12345678" = "0.005 ETH" [pricing_overrides.by_requestor_selector] -"0x0000000000000000000000000000000000000001:0x12345678" = { min_mcycle_price = "0.01 USD" } +"0x0000000000000000000000000000000000000001:0x12345678" = "0.01 USD" "#; let config: MarketConfig = toml::from_str(toml).unwrap(); diff --git a/crates/boundless-market/src/prover_utils/mod.rs b/crates/boundless-market/src/prover_utils/mod.rs index 7314e8074..ba537c2f8 100644 --- a/crates/boundless-market/src/prover_utils/mod.rs +++ b/crates/boundless-market/src/prover_utils/mod.rs @@ -23,7 +23,7 @@ pub use config::MarketConfig; #[cfg(feature = "prover_utils")] pub use config::{ defaults as config_defaults, BatcherConfig, Config, MarketConfig, OrderCommitmentPriority, - OrderPricingPriority, PricingOverrideEntry, PricingOverrides, ProverConfig, + OrderPricingPriority, PricingOverrides, ProverConfig, }; use crate::{ diff --git a/crates/broker/src/order_picker.rs b/crates/broker/src/order_picker.rs index f5de3896b..e593b3521 100644 --- a/crates/broker/src/order_picker.rs +++ b/crates/broker/src/order_picker.rs @@ -2980,12 +2980,10 @@ pub(crate) mod tests { // Now build with a requestor override: 10x the global min_mcycle_price let mut config2_market = config.lock_all().unwrap().market.clone(); - config2_market.pricing_overrides.by_requestor.insert( - requestor_addr, - boundless_market::prover_utils::PricingOverrideEntry { - min_mcycle_price: Amount::parse(override_price, None).unwrap(), - }, - ); + config2_market + .pricing_overrides + .by_requestor + .insert(requestor_addr, Amount::parse(override_price, None).unwrap()); let config2 = ConfigLock::default(); config2.load_write().unwrap().market = config2_market; @@ -3046,18 +3044,14 @@ pub(crate) mod tests { let selector = order_template.request.requirements.selector; let mut config2_market = config.lock_all().unwrap().market.clone(); - config2_market.pricing_overrides.by_requestor.insert( - requestor_addr, - boundless_market::prover_utils::PricingOverrideEntry { - min_mcycle_price: Amount::parse("0.005 ETH", None).unwrap(), - }, - ); - config2_market.pricing_overrides.by_selector.insert( - selector, - boundless_market::prover_utils::PricingOverrideEntry { - min_mcycle_price: Amount::parse("0.01 ETH", None).unwrap(), - }, - ); + config2_market + .pricing_overrides + .by_requestor + .insert(requestor_addr, Amount::parse("0.005 ETH", None).unwrap()); + config2_market + .pricing_overrides + .by_selector + .insert(selector, Amount::parse("0.01 ETH", None).unwrap()); let config2 = ConfigLock::default(); config2.load_write().unwrap().market = config2_market; From 45e58f4e3b83766a4e050c0b2b9e5d2e5b54f183 Mon Sep 17 00:00:00 2001 From: capossele Date: Fri, 20 Feb 2026 12:55:07 +0000 Subject: [PATCH 5/9] simplify --- .../boundless-market/src/prover_utils/mod.rs | 23 +++++++------------ crates/broker/src/order_picker.rs | 18 --------------- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/crates/boundless-market/src/prover_utils/mod.rs b/crates/boundless-market/src/prover_utils/mod.rs index ba537c2f8..3dac737a8 100644 --- a/crates/boundless-market/src/prover_utils/mod.rs +++ b/crates/boundless-market/src/prover_utils/mod.rs @@ -422,15 +422,6 @@ pub trait OrderPricingContext { /// Convert an Amount to ZKC using the price oracle. async fn convert_to_zkc(&self, amount: &Amount) -> Result; - /// Resolve the effective `min_mcycle_price` for an order. - /// - /// The default implementation returns `MarketConfig::min_mcycle_price`. - /// Override this to implement per-requestor / per-selector pricing (see [`config::PricingOverrides`]). - fn resolve_min_mcycle_price(&self, order: &OrderRequest) -> Result { - let _ = order; - Ok(self.market_config()?.min_mcycle_price.clone()) - } - /// Access to the prover for preflight operations. fn prover(&self) -> &ProverObj; @@ -941,9 +932,10 @@ pub trait OrderPricingContext { }) } else { // For lockable orders, evaluate based on ETH price. - // Use per-requestor/selector override if configured, otherwise global default. - let resolved_min_mcycle_price = self.resolve_min_mcycle_price(order)?; - let config_min_mcycle_price_amount = &resolved_min_mcycle_price; + let config_min_mcycle_price_amount = config + .pricing_overrides + .resolve(&order.request.client_address(), &order.request.requirements.selector) + .unwrap_or(&config.min_mcycle_price); // Convert configured price to ETH (i.e., handles USD via price oracle) let config_min_mcycle_price_eth = @@ -1056,9 +1048,10 @@ pub trait OrderPricingContext { let lock_expiry = order.request.lock_expires_at(); let order_expiry = order.request.expires_at(); let config = self.market_config()?; - // Use per-requestor/selector override if configured, otherwise global default. - let resolved_min_mcycle_price = self.resolve_min_mcycle_price(order)?; - let min_mcycle_price_amount = &resolved_min_mcycle_price; + let min_mcycle_price_amount = config + .pricing_overrides + .resolve(&order.request.client_address(), &order.request.requirements.selector) + .unwrap_or(&config.min_mcycle_price); // Convert configured price to ETH (handles USD via price oracle) let min_mcycle_price_eth = self.convert_to_eth(min_mcycle_price_amount).await?; diff --git a/crates/broker/src/order_picker.rs b/crates/broker/src/order_picker.rs index e593b3521..3ad175339 100644 --- a/crates/broker/src/order_picker.rs +++ b/crates/broker/src/order_picker.rs @@ -279,24 +279,6 @@ where Ok(config.market.deny_requestor_addresses.clone()) } - fn resolve_min_mcycle_price(&self, order: &OrderRequest) -> Result { - let config = self.market_config()?; - let requestor = order.request.client_address(); - let selector = order.request.requirements.selector; - if let Some(amount) = config.pricing_overrides.resolve(&requestor, &selector) { - tracing::debug!( - order_id = %order.id(), - %requestor, - %selector, - override_price = %amount, - global_price = %config.min_mcycle_price, - "Using pricing override instead of global min_mcycle_price" - ); - return Ok(amount.clone()); - } - Ok(config.min_mcycle_price.clone()) - } - fn supported_selectors(&self) -> &SupportedSelectors { &self.supported_selectors } From dda1504fe0984a935b0754c976d269792aefcafe Mon Sep 17 00:00:00 2001 From: capossele Date: Fri, 20 Feb 2026 12:57:17 +0000 Subject: [PATCH 6/9] revert compose --- compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/compose.yml b/compose.yml index f5b5c3c75..456eb8826 100644 --- a/compose.yml +++ b/compose.yml @@ -278,7 +278,6 @@ services: broker: <<: *broker-common - working_dir: /app # needed for source-build Dockerfile which doesn't set WORKDIR in the runtime stage volumes: - type: bind source: ./broker.toml From 435323853fe90391c48b0e0631ab89301d6b3059 Mon Sep 17 00:00:00 2001 From: capossele Date: Fri, 20 Feb 2026 13:02:06 +0000 Subject: [PATCH 7/9] trim tests --- .../src/prover_utils/config.rs | 50 ------------- crates/broker/src/order_picker.rs | 73 ------------------- 2 files changed, 123 deletions(-) diff --git a/crates/boundless-market/src/prover_utils/config.rs b/crates/boundless-market/src/prover_utils/config.rs index 4bd653072..6bdd7e700 100644 --- a/crates/boundless-market/src/prover_utils/config.rs +++ b/crates/boundless-market/src/prover_utils/config.rs @@ -822,40 +822,6 @@ mod tests { Amount::parse(s, None).unwrap() } - #[test] - fn test_resolve_empty_returns_none() { - let overrides = PricingOverrides::default(); - let result = overrides - .resolve(&addr("0x0000000000000000000000000000000000000001"), &sel("0x12345678")); - assert!(result.is_none()); - } - - #[test] - fn test_resolve_by_requestor() { - let requestor = addr("0x0000000000000000000000000000000000000001"); - let mut overrides = PricingOverrides::default(); - overrides.by_requestor.insert(requestor, amount("0.001 USD")); - - let result = overrides.resolve(&requestor, &sel("0xaabbccdd")); - assert_eq!(result.unwrap().to_string(), "0.001 USD"); - - let other = addr("0x0000000000000000000000000000000000000002"); - assert!(overrides.resolve(&other, &sel("0xaabbccdd")).is_none()); - } - - #[test] - fn test_resolve_by_selector() { - let selector = sel("0x12345678"); - let mut overrides = PricingOverrides::default(); - overrides.by_selector.insert(selector, amount("0.005 USD")); - - let any_addr = addr("0x0000000000000000000000000000000000000099"); - let result = overrides.resolve(&any_addr, &selector); - assert_eq!(result.unwrap().to_string(), "0.005 USD"); - - assert!(overrides.resolve(&any_addr, &sel("0x00000000")).is_none()); - } - #[test] fn test_resolve_priority_requestor_selector_over_individual() { let requestor = addr("0x0000000000000000000000000000000000000001"); @@ -954,14 +920,6 @@ mod tests { assert!(result.is_err()); } - #[test] - fn test_toml_empty_overrides() { - let overrides: PricingOverrides = toml::from_str("").unwrap(); - assert!(overrides.by_requestor.is_empty()); - assert!(overrides.by_selector.is_empty()); - assert!(overrides.by_requestor_selector.is_empty()); - } - #[test] fn test_market_config_toml_with_pricing_overrides() { let toml = r#" @@ -1003,12 +961,4 @@ max_stake = "10 USD" "0.005 ETH" ); } - - #[test] - fn test_market_config_default_has_empty_pricing_overrides() { - let config = MarketConfig::default(); - assert!(config.pricing_overrides.by_requestor.is_empty()); - assert!(config.pricing_overrides.by_selector.is_empty()); - assert!(config.pricing_overrides.by_requestor_selector.is_empty()); - } } diff --git a/crates/broker/src/order_picker.rs b/crates/broker/src/order_picker.rs index 3ad175339..ab1f307dc 100644 --- a/crates/broker/src/order_picker.rs +++ b/crates/broker/src/order_picker.rs @@ -2923,79 +2923,6 @@ pub(crate) mod tests { assert!(priced_order.target_timestamp.is_some()); } - #[tokio::test] - async fn test_calculate_exec_limits_with_requestor_pricing_override() { - let global_price = "0.001 ETH"; - let override_price = "0.01 ETH"; - - let mut market_config = MarketConfig::default(); - market_config.min_mcycle_price = Amount::parse(global_price, None).unwrap(); - market_config.min_mcycle_price_collateral_token = Amount::parse("10 ZKC", None).unwrap(); - market_config.max_mcycle_limit = 100_000; - - let config = ConfigLock::default(); - config.load_write().unwrap().market = market_config; - - // Build without overrides first to get the baseline - let ctx_no_override = - PickerTestCtxBuilder::default().with_config(config.clone()).build().await; - - let gas_cost = parse_ether("0.001").unwrap(); - let order = ctx_no_override - .generate_next_order(OrderParams { - fulfillment_type: FulfillmentType::LockAndFulfill, - max_price: parse_ether("0.05").unwrap(), - lock_collateral: parse_collateral_tokens("100"), - lock_timeout: 900, - timeout: 1200, - ..Default::default() - }) - .await; - - let requestor_addr = order.request.client_address(); - - let (_, baseline_prove_limit, _) = - ctx_no_override.picker.calculate_exec_limits(&order, gas_cost).await.unwrap(); - - // ETH based: (0.05 - 0.001) * 1M / 0.001 = 49M cycles - assert_eq!(baseline_prove_limit, 49_000_000u64); - - // Now build with a requestor override: 10x the global min_mcycle_price - let mut config2_market = config.lock_all().unwrap().market.clone(); - config2_market - .pricing_overrides - .by_requestor - .insert(requestor_addr, Amount::parse(override_price, None).unwrap()); - let config2 = ConfigLock::default(); - config2.load_write().unwrap().market = config2_market; - - let ctx_with_override = PickerTestCtxBuilder::default().with_config(config2).build().await; - - let order2 = ctx_with_override - .generate_next_order(OrderParams { - fulfillment_type: FulfillmentType::LockAndFulfill, - max_price: parse_ether("0.05").unwrap(), - lock_collateral: parse_collateral_tokens("100"), - lock_timeout: 900, - timeout: 1200, - ..Default::default() - }) - .await; - - let (_, overridden_prove_limit, _) = - ctx_with_override.picker.calculate_exec_limits(&order2, gas_cost).await.unwrap(); - - // ETH based with override: (0.05 - 0.001) * 1M / 0.01 = 4.9M cycles - assert_eq!(overridden_prove_limit, 4_900_000u64); - - // The override should produce a stricter (lower) limit - assert!( - overridden_prove_limit < baseline_prove_limit, - "Override price {override_price} should produce a lower exec limit than global {global_price}: \ - got {overridden_prove_limit} vs {baseline_prove_limit}" - ); - } - #[tokio::test] async fn test_calculate_exec_limits_selector_override_takes_priority() { let mut market_config = MarketConfig::default(); From 346f7e81cd7aee5afa8a1c8b5b696e1d78e6aa5e Mon Sep 17 00:00:00 2001 From: capossele Date: Fri, 20 Feb 2026 15:32:41 +0000 Subject: [PATCH 8/9] feedback --- broker-template.toml | 8 +- .../src/prover_utils/config.rs | 125 ++++++++++-------- crates/broker/src/order_picker.rs | 14 +- 3 files changed, 78 insertions(+), 69 deletions(-) diff --git a/broker-template.toml b/broker-template.toml index cf11bef05..8debb1225 100644 --- a/broker-template.toml +++ b/broker-template.toml @@ -26,9 +26,9 @@ min_mcycle_price = "0.00002 USD" -# Per-requestor and per-selector min_mcycle_price overrides. +# Per-requestor and per-proof-type min_mcycle_price overrides. # -# Resolution priority: by_requestor_selector > by_selector > by_requestor > global default. +# Resolution priority: by_requestor_proof_type > by_proof_type > by_requestor > global default. # Changes take effect immediately when the broker reloads this file. # # Uncomment and populate any of the sections below to enable overrides: @@ -36,10 +36,10 @@ min_mcycle_price = "0.00002 USD" # [market.pricing_overrides.by_requestor] # "0xAbC...123" = "0.0001 USD" # -# [market.pricing_overrides.by_selector] +# [market.pricing_overrides.by_proof_type] # "0x12345678" = "0.0005 USD" # -# [market.pricing_overrides.by_requestor_selector] +# [market.pricing_overrides.by_requestor_proof_type] # "0xAbC...123:0x12345678" = "0.001 USD" # The minimum price per mega-cycle (i.e. million RISC-V cycles) for the broker diff --git a/crates/boundless-market/src/prover_utils/config.rs b/crates/boundless-market/src/prover_utils/config.rs index cb1be9802..bc7832a8c 100644 --- a/crates/boundless-market/src/prover_utils/config.rs +++ b/crates/boundless-market/src/prover_utils/config.rs @@ -516,10 +516,13 @@ pub struct MarketConfig { /// market. This should remain false to avoid losing partial PoVW jobs. #[serde(default)] pub cancel_proving_expired_orders: bool, - /// Per-requestor and per-selector `min_mcycle_price` overrides. + /// Per-requestor and per-proof-type `min_mcycle_price` overrides. + /// + /// The proof type is the 4-byte function selector from the order's requirements + /// (`request.requirements.selector`), identifying the verifier entry point. /// /// When the broker evaluates an order, it resolves the effective `min_mcycle_price` - /// with priority: `by_requestor_selector` > `by_selector` > `by_requestor` > global default. + /// with priority: `by_requestor_proof_type` > `by_proof_type` > `by_requestor` > global default. /// Changes to this section are picked up automatically when the broker reloads `broker.toml`. #[serde(default)] pub pricing_overrides: PricingOverrides, @@ -755,11 +758,14 @@ impl Config { } } -/// Per-requestor and per-selector `min_mcycle_price` overrides. +/// Per-requestor and per-proof-type `min_mcycle_price` overrides. +/// +/// The proof type is the 4-byte function selector from the order's requirements +/// (i.e. `request.requirements.selector`), which identifies the verifier entry point. /// /// Resolution priority (first match wins): -/// 1. `by_requestor_selector` -- keyed by `"
:"` -/// 2. `by_selector` -- keyed by selector hex (e.g. `"0x12345678"`) +/// 1. `by_requestor_proof_type` -- keyed by `"
:"` +/// 2. `by_proof_type` -- keyed by proof type hex (e.g. `"0x12345678"`) /// 3. `by_requestor` -- keyed by requestor address /// 4. Fall back to global `MarketConfig::min_mcycle_price` #[derive(Debug, Clone, Default, Deserialize, Serialize)] @@ -767,19 +773,19 @@ pub struct PricingOverrides { /// Overrides keyed by requestor address (checksummed or lowercase hex). #[serde(default)] pub by_requestor: HashMap, - /// Overrides keyed by selector hex (e.g. `"0x12345678"`). + /// Overrides keyed by proof type hex (e.g. `"0x12345678"`). #[serde(default)] - pub by_selector: HashMap, Amount>, - /// Overrides keyed by `":"` for the most specific match. + pub by_proof_type: HashMap, Amount>, + /// Overrides keyed by `":"` for the most specific match. #[serde( default, - deserialize_with = "deserialize_requestor_selector_map", - serialize_with = "serialize_requestor_selector_map" + deserialize_with = "deserialize_requestor_proof_type_map", + serialize_with = "serialize_requestor_proof_type_map" )] - pub by_requestor_selector: HashMap<(Address, FixedBytes<4>), Amount>, + pub by_requestor_proof_type: HashMap<(Address, FixedBytes<4>), Amount>, } -fn deserialize_requestor_selector_map<'de, D>( +fn deserialize_requestor_proof_type_map<'de, D>( deserializer: D, ) -> Result), Amount>, D::Error> where @@ -788,19 +794,19 @@ where let raw: HashMap = HashMap::deserialize(deserializer)?; let mut map = HashMap::with_capacity(raw.len()); for (key, amount) in raw { - let (addr_str, sel_str) = key.split_once(':').ok_or_else(|| { + let (addr_str, proof_type_str) = key.split_once(':').ok_or_else(|| { serde::de::Error::custom(format!( - "by_requestor_selector key must be '
:', got '{key}'" + "by_requestor_proof_type key must be '
:', got '{key}'" )) })?; let addr: Address = addr_str.parse().map_err(serde::de::Error::custom)?; - let sel: FixedBytes<4> = sel_str.parse().map_err(serde::de::Error::custom)?; - map.insert((addr, sel), amount); + let proof_type: FixedBytes<4> = proof_type_str.parse().map_err(serde::de::Error::custom)?; + map.insert((addr, proof_type), amount); } Ok(map) } -fn serialize_requestor_selector_map( +fn serialize_requestor_proof_type_map( map: &HashMap<(Address, FixedBytes<4>), Amount>, serializer: S, ) -> Result @@ -809,20 +815,20 @@ where { use serde::ser::SerializeMap; let mut ser_map = serializer.serialize_map(Some(map.len()))?; - for ((addr, sel), amount) in map { - ser_map.serialize_entry(&format!("{addr}:{sel}"), amount)?; + for ((addr, proof_type), amount) in map { + ser_map.serialize_entry(&format!("{addr}:{proof_type}"), amount)?; } ser_map.end() } impl PricingOverrides { - /// Resolve the effective `min_mcycle_price` for a given requestor and selector. + /// Resolve the effective `min_mcycle_price` for a given requestor and proof type. /// /// Returns `Some(amount)` if an override matches, `None` to use the global default. - pub fn resolve(&self, requestor: &Address, selector: &FixedBytes<4>) -> Option<&Amount> { - self.by_requestor_selector - .get(&(*requestor, *selector)) - .or_else(|| self.by_selector.get(selector)) + pub fn resolve(&self, requestor: &Address, proof_type: &FixedBytes<4>) -> Option<&Amount> { + self.by_requestor_proof_type + .get(&(*requestor, *proof_type)) + .or_else(|| self.by_proof_type.get(proof_type)) .or_else(|| self.by_requestor.get(requestor)) } } @@ -844,29 +850,29 @@ mod tests { } #[test] - fn test_resolve_priority_requestor_selector_over_individual() { + fn test_resolve_priority_requestor_proof_type_over_individual() { let requestor = addr("0x0000000000000000000000000000000000000001"); - let selector = sel("0x12345678"); + let proof_type = sel("0x12345678"); let mut overrides = PricingOverrides::default(); overrides.by_requestor.insert(requestor, amount("0.001 USD")); - overrides.by_selector.insert(selector, amount("0.002 USD")); - overrides.by_requestor_selector.insert((requestor, selector), amount("0.003 USD")); + overrides.by_proof_type.insert(proof_type, amount("0.002 USD")); + overrides.by_requestor_proof_type.insert((requestor, proof_type), amount("0.003 USD")); - let result = overrides.resolve(&requestor, &selector); + let result = overrides.resolve(&requestor, &proof_type); assert_eq!(result.unwrap().to_string(), "0.003 USD"); } #[test] - fn test_resolve_selector_over_requestor() { + fn test_resolve_proof_type_over_requestor() { let requestor = addr("0x0000000000000000000000000000000000000001"); - let selector = sel("0x12345678"); + let proof_type = sel("0x12345678"); let mut overrides = PricingOverrides::default(); overrides.by_requestor.insert(requestor, amount("0.001 USD")); - overrides.by_selector.insert(selector, amount("0.002 USD")); + overrides.by_proof_type.insert(proof_type, amount("0.002 USD")); - let result = overrides.resolve(&requestor, &selector); + let result = overrides.resolve(&requestor, &proof_type); assert_eq!(result.unwrap().to_string(), "0.002 USD"); } @@ -876,30 +882,33 @@ mod tests { [by_requestor] "0x0000000000000000000000000000000000000001" = "0.001 USD" -[by_selector] +[by_proof_type] "0x12345678" = "0.005 ETH" -[by_requestor_selector] +[by_requestor_proof_type] "0x0000000000000000000000000000000000000001:0x12345678" = "0.01 USD" "#; let overrides: PricingOverrides = toml::from_str(toml).unwrap(); assert_eq!(overrides.by_requestor.len(), 1); - assert_eq!(overrides.by_selector.len(), 1); - assert_eq!(overrides.by_requestor_selector.len(), 1); + assert_eq!(overrides.by_proof_type.len(), 1); + assert_eq!(overrides.by_requestor_proof_type.len(), 1); let requestor = addr("0x0000000000000000000000000000000000000001"); - let selector = sel("0x12345678"); + let proof_type = sel("0x12345678"); - assert_eq!(overrides.resolve(&requestor, &selector).unwrap().to_string(), "0.01 USD"); + assert_eq!(overrides.resolve(&requestor, &proof_type).unwrap().to_string(), "0.01 USD"); - let other_sel = sel("0xdeadbeef"); - assert_eq!(overrides.resolve(&requestor, &other_sel).unwrap().to_string(), "0.001 USD"); + let other_proof_type = sel("0xdeadbeef"); + assert_eq!( + overrides.resolve(&requestor, &other_proof_type).unwrap().to_string(), + "0.001 USD" + ); let other_addr = addr("0x0000000000000000000000000000000000000099"); - assert_eq!(overrides.resolve(&other_addr, &selector).unwrap().to_string(), "0.005 ETH"); + assert_eq!(overrides.resolve(&other_addr, &proof_type).unwrap().to_string(), "0.005 ETH"); - assert!(overrides.resolve(&other_addr, &other_sel).is_none()); + assert!(overrides.resolve(&other_addr, &other_proof_type).is_none()); } #[test] @@ -909,32 +918,32 @@ mod tests { "0x0000000000000000000000000000000000000001" = "0.001 USD" "0x0000000000000000000000000000000000000002" = "0.002 USD" -[by_selector] +[by_proof_type] "0x12345678" = "0.005 ETH" "0xdeadbeef" = "0.01 ETH" -[by_requestor_selector] +[by_requestor_proof_type] "0x0000000000000000000000000000000000000001:0x12345678" = "0.001 USD" "0x0000000000000000000000000000000000000002:0xdeadbeef" = "0.002 USD" "#; let overrides: PricingOverrides = toml::from_str(toml).unwrap(); assert_eq!(overrides.by_requestor.len(), 2); - assert_eq!(overrides.by_selector.len(), 2); - assert_eq!(overrides.by_requestor_selector.len(), 2); + assert_eq!(overrides.by_proof_type.len(), 2); + assert_eq!(overrides.by_requestor_proof_type.len(), 2); // Verify serialize → deserialize round-trip preserves all entries let serialized = toml::to_string(&overrides).unwrap(); let deserialized: PricingOverrides = toml::from_str(&serialized).unwrap(); assert_eq!(deserialized.by_requestor.len(), 2); - assert_eq!(deserialized.by_selector.len(), 2); - assert_eq!(deserialized.by_requestor_selector.len(), 2); + assert_eq!(deserialized.by_proof_type.len(), 2); + assert_eq!(deserialized.by_requestor_proof_type.len(), 2); } #[test] - fn test_toml_invalid_requestor_selector_key() { + fn test_toml_invalid_requestor_proof_type_key() { let toml = r#" -[by_requestor_selector] +[by_requestor_proof_type] "bad-key-no-colon" = "0.001 USD" "#; let result: Result = toml::from_str(toml); @@ -951,20 +960,20 @@ max_stake = "10 USD" [pricing_overrides.by_requestor] "0x0000000000000000000000000000000000000001" = "0.001 USD" -[pricing_overrides.by_selector] +[pricing_overrides.by_proof_type] "0x12345678" = "0.005 ETH" -[pricing_overrides.by_requestor_selector] +[pricing_overrides.by_requestor_proof_type] "0x0000000000000000000000000000000000000001:0x12345678" = "0.01 USD" "#; let config: MarketConfig = toml::from_str(toml).unwrap(); let requestor = addr("0x0000000000000000000000000000000000000001"); - let selector = sel("0x12345678"); + let proof_type = sel("0x12345678"); - // requestor+selector combo takes highest priority + // requestor+proof_type combo takes highest priority assert_eq!( - config.pricing_overrides.resolve(&requestor, &selector).unwrap().to_string(), + config.pricing_overrides.resolve(&requestor, &proof_type).unwrap().to_string(), "0.01 USD" ); // requestor-only match @@ -972,11 +981,11 @@ max_stake = "10 USD" config.pricing_overrides.resolve(&requestor, &sel("0xdeadbeef")).unwrap().to_string(), "0.001 USD" ); - // selector-only match + // proof_type-only match assert_eq!( config .pricing_overrides - .resolve(&addr("0x0000000000000000000000000000000000000099"), &selector) + .resolve(&addr("0x0000000000000000000000000000000000000099"), &proof_type) .unwrap() .to_string(), "0.005 ETH" diff --git a/crates/broker/src/order_picker.rs b/crates/broker/src/order_picker.rs index 40f26cf47..e7708c793 100644 --- a/crates/broker/src/order_picker.rs +++ b/crates/broker/src/order_picker.rs @@ -3077,7 +3077,7 @@ pub(crate) mod tests { } #[tokio::test] - async fn test_calculate_exec_limits_selector_override_takes_priority() { + async fn test_calculate_exec_limits_proof_type_override_takes_priority() { let mut market_config = MarketConfig::default(); market_config.min_mcycle_price = Amount::parse("0.001 ETH", None).unwrap(); market_config.min_mcycle_price_collateral_token = Amount::parse("10 ZKC", None).unwrap(); @@ -3086,8 +3086,8 @@ pub(crate) mod tests { let config = ConfigLock::default(); config.load_write().unwrap().market = market_config; - // Set up overrides: requestor gets 0.005 ETH, but selector gets 0.01 ETH. - // Selector should win per the cascade priority. + // Set up overrides: requestor gets 0.005 ETH, but proof_type gets 0.01 ETH. + // proof_type should win per the cascade priority. let ctx_baseline = PickerTestCtxBuilder::default().with_config(config.clone()).build().await; @@ -3103,7 +3103,7 @@ pub(crate) mod tests { .await; let requestor_addr = order_template.request.client_address(); - let selector = order_template.request.requirements.selector; + let proof_type = order_template.request.requirements.selector; let mut config2_market = config.lock_all().unwrap().market.clone(); config2_market @@ -3112,8 +3112,8 @@ pub(crate) mod tests { .insert(requestor_addr, Amount::parse("0.005 ETH", None).unwrap()); config2_market .pricing_overrides - .by_selector - .insert(selector, Amount::parse("0.01 ETH", None).unwrap()); + .by_proof_type + .insert(proof_type, Amount::parse("0.01 ETH", None).unwrap()); let config2 = ConfigLock::default(); config2.load_write().unwrap().market = config2_market; @@ -3133,7 +3133,7 @@ pub(crate) mod tests { let (_, prove_limit, _) = ctx.picker.calculate_exec_limits(&order, gas_cost).await.unwrap(); - // Selector override (0.01 ETH) should take priority over requestor (0.005 ETH). + // proof_type override (0.01 ETH) should take priority over requestor (0.005 ETH). // ETH based: (0.05 - 0.001) * 1M / 0.01 = 4.9M cycles assert_eq!(prove_limit, 4_900_000u64); } From 4be3bcff711c9ef512af074ce2892dd777a11666 Mon Sep 17 00:00:00 2001 From: capossele Date: Fri, 20 Feb 2026 15:34:10 +0000 Subject: [PATCH 9/9] typo --- crates/boundless-market/src/prover_utils/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/boundless-market/src/prover_utils/config.rs b/crates/boundless-market/src/prover_utils/config.rs index bc7832a8c..94af46fca 100644 --- a/crates/boundless-market/src/prover_utils/config.rs +++ b/crates/boundless-market/src/prover_utils/config.rs @@ -760,7 +760,7 @@ impl Config { /// Per-requestor and per-proof-type `min_mcycle_price` overrides. /// -/// The proof type is the 4-byte function selector from the order's requirements +/// The proof type is the 4-byte selector from the order's requirements /// (i.e. `request.requirements.selector`), which identifies the verifier entry point. /// /// Resolution priority (first match wins):