Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions crates/boundless-market/src/prover_utils/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,27 @@ pub struct MarketConfig {
/// Orders under this min_cycles will be skipped after preflight
#[serde(default = "defaults::min_mcycle_limit")]
pub min_mcycle_limit: u64,
/// Optional priority requestor addresses that can bypass the mcycle limit and max input size limit.
/// Optional static list of priority requestor addresses.
///
/// If enabled, the order will be preflighted without constraints.
/// Orders from these addresses:
/// 1. Resource-limit bypass: `max_mcycle_limit` and `max_file_size` are ignored.
/// 2. Ordering priority: sorted ahead of regular orders in both pricing and
/// commitment selection (within that group, the configured priority mode applies).
///
/// Limits that still apply to priority requestors: gas-based cycle limits,
/// deadline caps (`peak_prove_khz`), `max_journal_bytes`, `max_collateral`,
/// `min_deadline`, and selector requirements.
///
/// Merged with addresses fetched from `priority_requestor_lists`.
#[serde(alias = "priority_requestor_addresses")]
pub priority_requestor_addresses: Option<Vec<Address>>,
/// Optional URLs to fetch requestor priority lists from.
/// URLs to fetch remote priority requestor lists from.
///
/// Lists are refreshed every hour and merged with `priority_requestor_addresses`.
/// When multiple URLs contain the same address, the first URL in this array takes
/// precedence (lists are processed in reverse so earlier entries overwrite later ones).
///
/// These lists will be periodically refreshed and merged with priority_requestor_addresses.
/// Defaults to the Boundless-recommended priority list.
#[serde(default = "defaults::priority_requestor_lists")]
pub priority_requestor_lists: Option<Vec<String>>,
/// Max journal size in bytes
Expand Down Expand Up @@ -370,18 +383,34 @@ pub struct MarketConfig {
/// Requests that require a higher collateral amount than this will not be considered.
#[serde(alias = "max_stake", deserialize_with = "deserialize_max_collateral")]
pub max_collateral: Amount,
/// Optional allow list for customer address.
/// Static allow list of requestor addresses (whitelist).
///
/// When any allow source is configured (this field or `allow_requestor_lists`),
/// only orders from listed addresses are accepted; all others are skipped.
/// When neither is set, all requestors are accepted.
///
/// Allow-listed requestors also bypass `max_mcycle_limit` and `max_file_size`,
/// the same resource-limit bypass that priority requestors receive.
///
/// Limits that still apply: gas-based cycle limits, deadline caps
/// (`peak_prove_khz`), `max_journal_bytes`, `max_collateral`, `min_deadline`,
/// and selector requirements.
///
/// If enabled, all requests from clients not in the allow list are skipped.
pub allow_client_addresses: Option<Vec<Address>>,
/// Optional URLs to fetch requestor allow lists from.
/// Merged with addresses fetched from `allow_requestor_lists`.
#[serde(alias = "allow_client_addresses")]
pub allow_requestor_addresses: Option<Vec<Address>>,
/// URLs to fetch remote allow requestor lists from.
///
/// These lists will be periodically refreshed and merged with allow_client_addresses.
/// Lists are refreshed every hour and merged with `allow_requestor_addresses`.
/// Multiple lists are unioned: all addresses from all lists are allowed
/// (order-independent, unlike priority lists).
#[serde(default = "defaults::allow_requestor_lists")]
pub allow_requestor_lists: Option<Vec<String>>,
/// Optional deny list for requestor address.
/// Static deny list of requestor addresses (blacklist).
///
/// If enabled, all requests from clients in the deny list are skipped.
/// Orders from these addresses are unconditionally skipped, even if the
/// address also appears on an allow or priority list. The deny check runs
/// after the allow check.
pub deny_requestor_addresses: Option<HashSet<Address>>,
/// Transaction priority mode (low, medium, high, or custom)
///
Expand Down Expand Up @@ -544,7 +573,7 @@ impl Default for MarketConfig {
events_poll_blocks: defaults::events_poll_blocks(),
events_poll_ms: defaults::events_poll_ms(),
max_collateral: Amount::parse("10 USD", None).expect("valid default"),
allow_client_addresses: None,
allow_requestor_addresses: None,
deny_requestor_addresses: None,
gas_priority_mode: defaults::priority_mode(),
lockin_priority_gas: None,
Expand Down
62 changes: 28 additions & 34 deletions crates/boundless-market/src/prover_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ use risc0_zkvm::sha::Digest as Risc0Digest;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
collections::HashSet,
fmt,
sync::{Arc, OnceLock},
};
Expand Down Expand Up @@ -376,13 +375,14 @@ async fn upload_image_with_downloader(
/// This is a standalone function (not a trait method) so it can be called from inside
/// async closures like `try_get_with` without capturing `&self`.
///
/// If `is_priority_requestor` is true, size limits are bypassed when fetching from URLs.
/// If `skip_size_limit` is true, the configured max_file_size is bypassed when fetching from URLs.
/// This applies to priority and allow-listed requestors.
async fn upload_input_with_downloader(
prover: &ProverObj,
input_type: crate::contracts::RequestInputType,
input_data: &Bytes,
downloader: &(dyn StorageDownloader + Send + Sync),
is_priority_requestor: bool,
skip_size_limit: bool,
) -> anyhow::Result<String> {
match input_type {
crate::contracts::RequestInputType::Inline => {
Expand All @@ -394,7 +394,7 @@ async fn upload_input_with_downloader(
std::str::from_utf8(input_data).context("input url is not valid utf8")?;

tracing::debug!("Fetching input from URI {input_url}");
let raw_input = if is_priority_requestor {
let raw_input = if skip_size_limit {
downloader.download_with_limit(input_url, usize::MAX).await
} else {
downloader.download(input_url).await
Expand Down Expand Up @@ -422,14 +422,13 @@ pub trait OrderPricingContext {
fn format_collateral(&self, value: U256) -> String {
format_units(value, self.collateral_token_decimals()).unwrap_or_else(|_| "?".to_string())
}
/// Returns the set of denied requestor addresses, if any are configured.
fn denied_requestor_addresses(&self) -> Result<Option<HashSet<Address>>, OrderPricingError> {
Ok(None)
}
fn check_requestor_allowed(
fn check_access_lists(
&self,
order: &OrderRequest,
) -> Result<Option<OrderPricingOutcome>, OrderPricingError>;
fn check_supported_selectors(
&self,
order: &OrderRequest,
denied_addresses_opt: Option<&HashSet<Address>>,
) -> Result<Option<OrderPricingOutcome>, OrderPricingError>;
async fn check_request_available(
&self,
Expand All @@ -438,6 +437,12 @@ pub trait OrderPricingContext {
#[cfg(feature = "prover_utils")]
async fn estimate_gas_to_fulfill_pending(&self) -> Result<u64, OrderPricingError>;
fn is_priority_requestor(&self, client_addr: &Address) -> bool;
fn is_allow_requestor(&self, client_addr: &Address) -> bool;
/// Returns true if the requestor is on either the priority or allow list.
/// Used to bypass resource limits (max_mcycle_limit, max_file_size).
fn skip_resource_limits(&self, client_addr: &Address) -> bool {
self.is_priority_requestor(client_addr) || self.is_allow_requestor(client_addr)
}
async fn check_available_balances(
&self,
order: &OrderRequest,
Expand Down Expand Up @@ -502,13 +507,13 @@ pub trait OrderPricingContext {
/// Upload input data to the prover (from inline data or URL) using the downloader.
#[cfg(feature = "prover_utils")]
async fn upload_input(&self, order: &OrderRequest) -> Result<String, OrderPricingError> {
let is_priority = self.is_priority_requestor(&order.request.client_address());
let skip_limits = self.skip_resource_limits(&order.request.client_address());
upload_input_with_downloader(
self.prover(),
order.request.input.inputType,
&order.request.input.data,
self.downloader().as_ref(),
is_priority,
skip_limits,
)
.await
.map_err(|e| OrderPricingError::FetchInputErr(Arc::new(e)))
Expand All @@ -532,7 +537,7 @@ pub trait OrderPricingContext {
let input_type = order.request.input.inputType;
let input_data = order.request.input.data.clone();
let order_id_clone = order_id.clone();
let is_priority = self.is_priority_requestor(&order.request.client_address());
let skip_limits = self.skip_resource_limits(&order.request.client_address());

// Multiple concurrent calls of this coalesce into a single execution.
// https://docs.rs/moka/latest/moka/future/struct.Cache.html#concurrent-calls-on-the-same-key
Expand All @@ -550,7 +555,7 @@ pub trait OrderPricingContext {

// Upload input using downloader
let input_id =
upload_input_with_downloader(&prover, input_type, &input_data, downloader.as_ref(), is_priority)
upload_input_with_downloader(&prover, input_type, &input_data, downloader.as_ref(), skip_limits)
.await
.map_err(|e| OrderPricingError::FetchInputErr(Arc::new(e)))?;

Expand Down Expand Up @@ -633,7 +638,6 @@ pub trait OrderPricingContext {

let config = self.market_config()?;
let min_deadline = config.min_deadline;
let denied_addresses_opt = self.denied_requestor_addresses()?;

// Does the order expire within the min deadline
let seconds_left = expiration.saturating_sub(now);
Expand All @@ -656,24 +660,14 @@ pub trait OrderPricingContext {
}
}

// Check if requestor is allowed (from both static config and dynamic lists)
if let Some(outcome) = self.check_requestor_allowed(order, denied_addresses_opt.as_ref())? {
// Check allow and deny list access controls.
if let Some(outcome) = self.check_access_lists(order)? {
return Ok(outcome);
}

if !self.supported_selectors().is_supported(order.request.requirements.selector) {
return Ok(Skip {
reason: format!(
"unsupported selector requirement. Requested: {:x}. Supported: {:?}",
order.request.requirements.selector,
self.supported_selectors()
.selectors
.iter()
.map(|(k, v)| format!("{k:x} ({v:?})"))
.collect::<Vec<_>>()
),
});
};
if let Some(outcome) = self.check_supported_selectors(order)? {
return Ok(outcome);
}

// Check if the collateral is sane and if we can afford it
// For lock expired orders, we don't check the max collateral because we can't lock those orders.
Expand Down Expand Up @@ -1221,12 +1215,12 @@ pub trait OrderPricingContext {
"preflight_limit ({preflight_limit}) < prove_limit ({prove_limit})",
);

// Apply max mcycle limit cap
// Check if priority requestor address - skip all exec limit calculations
// Priority and allow-listed requestors bypass the max_mcycle_limit config cap.
// Gas-based limits and deadline-based limits still apply.
let client_addr = order.request.client_address();
let skip_mcycle_limit = self.is_priority_requestor(&client_addr);
let skip_mcycle_limit = self.skip_resource_limits(&client_addr);
if skip_mcycle_limit {
tracing::debug!("Order {order_id} exec limit config ignored due to client {} being part of priority requestors.", client_addr);
tracing::debug!("Order {order_id} max_mcycle_limit config ignored: client {} is on the priority or allow list.", client_addr);
}

if !skip_mcycle_limit {
Expand Down
15 changes: 12 additions & 3 deletions crates/boundless-market/src/prover_utils/requestor_pricing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
//! It uses the same pricing logic that provers use, but with minimal defaults
//! appropriate for requestor-side checks.

use std::collections::HashSet;
use std::sync::Arc;

use alloy::network::Ethereum;
Expand Down Expand Up @@ -260,10 +259,16 @@ where
self.collateral_token_decimals
}

fn check_requestor_allowed(
fn check_access_lists(
&self,
_order: &OrderRequest,
) -> Result<Option<OrderPricingOutcome>, OrderPricingError> {
Ok(None)
}

fn check_supported_selectors(
&self,
_order: &OrderRequest,
_denied_addresses_opt: Option<&HashSet<Address>>,
) -> Result<Option<OrderPricingOutcome>, OrderPricingError> {
Ok(None)
}
Expand All @@ -285,6 +290,10 @@ where
false
}

fn is_allow_requestor(&self, _client_addr: &Address) -> bool {
false
}

async fn check_available_balances(
&self,
_order: &OrderRequest,
Expand Down
4 changes: 2 additions & 2 deletions crates/broker/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ lookback_blocks = 100
max_stake = "0.1 ZKC"
max_file_size = 50_000_000
max_fetch_retries = 10
allow_client_addresses = ["0x0000000000000000000000000000000000000000"]
allow_requestor_addresses = ["0x0000000000000000000000000000000000000000"]
deny_requestor_addresses = ["0x0000000000000000000000000000000000000000"]
gas_priority_mode = "high"
max_mcycle_limit = 10
Expand Down Expand Up @@ -361,7 +361,7 @@ error = ?"#;
assert_eq!(config.market.peak_prove_khz, Some(10000));
assert_eq!(config.market.min_deadline, 300);
assert_eq!(config.market.lookback_blocks, 100);
assert_eq!(config.market.allow_client_addresses, Some(vec![Address::ZERO]));
assert_eq!(config.market.allow_requestor_addresses, Some(vec![Address::ZERO]));
assert_eq!(
config.market.deny_requestor_addresses,
Some([Address::ZERO].into_iter().collect())
Expand Down
1 change: 1 addition & 0 deletions crates/broker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,7 @@ where
config.clone(),
order_state_tx.clone(),
self.priority_requestors.clone(),
self.allow_requestors.clone(),
market.clone(),
self.downloader.clone(),
));
Expand Down
Loading
Loading