Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c0ffb36
switch broker to use consistent fee estimation logic
austinabell Feb 6, 2026
d72274e
test gas estimates above actual amounts
austinabell Feb 6, 2026
8caba8e
swap offer layer gas estimation from eth_gasPrice
austinabell Feb 6, 2026
2d22d86
update gas cost estimate to use higher percentile
austinabell Feb 6, 2026
4e39ebc
Merge branch 'main' into austin/estimate_gas_consistent
austinabell Feb 6, 2026
781c92f
adjust static test values
austinabell Feb 7, 2026
e163644
add back 2x multiplier for gas into max price
austinabell Feb 10, 2026
2b5e6c5
set test base fee
austinabell Feb 10, 2026
14ab0ca
Merge branch 'main' of github.com:boundless-xyz/boundless into austin…
austinabell Feb 10, 2026
7b0db14
Account for journal size costs in requestor/prover (#1624)
austinabell Feb 17, 2026
8334e0e
Merge branch 'main' into austin/estimate_gas_consistent
capossele Feb 18, 2026
7883b7d
fmt
capossele Feb 18, 2026
ca5c240
fixes
capossele Feb 18, 2026
69149b4
fix
capossele Feb 18, 2026
0e85409
adjustments
capossele Feb 19, 2026
e7fe599
add LightPreLockChecker
capossele Feb 19, 2026
ccdd824
fix test
capossele Feb 19, 2026
bc0051d
Merge branch 'main' into austin/estimate_gas_consistent
capossele Feb 19, 2026
5efcf89
log
capossele Feb 19, 2026
cf34bd5
improvements
capossele Feb 20, 2026
63b1209
simplify code
capossele Feb 20, 2026
0b758d4
typo
capossele Feb 20, 2026
2c1b620
typo
capossele Feb 20, 2026
bb382e2
comment
capossele Feb 20, 2026
eb21163
drop reset_pre_lock_retries
capossele Feb 20, 2026
babb50a
simplify match
capossele Feb 20, 2026
9bee28f
simplify
capossele Feb 20, 2026
7ea8898
use matches!
capossele Feb 20, 2026
71744fc
clean up
capossele Feb 20, 2026
40146a7
Merge branch 'austin/estimate_gas_consistent' into angelo/bm-2449-che…
capossele Feb 20, 2026
32f9aad
revert
capossele Feb 20, 2026
b275daf
drop pre lock retries
capossele Feb 20, 2026
72ce9d1
drop import
capossele Feb 20, 2026
d90e73f
Merge remote-tracking branch 'origin/main' into angelo/bm-2449-check-…
capossele Feb 20, 2026
10c2a37
Merge branch 'main' into angelo/bm-2449-check-gas-prices-at-lock-skip
capossele Feb 24, 2026
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
8 changes: 8 additions & 0 deletions crates/boundless-market/src/prover_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ pub struct OrderRequest {
pub journal_bytes: Option<usize>,
pub target_timestamp: Option<u64>,
pub expire_timestamp: Option<u64>,
/// Total gas units (lock + fulfill) estimated during pricing. Stored so the pre-lock
/// check can simply re-multiply by the current gas price instead of re-computing.
pub gas_estimate: Option<u64>,
pub expected_reward_eth: Option<U256>,
#[serde(skip)]
cached_id: OnceLock<String>,
Expand All @@ -217,6 +220,7 @@ impl OrderRequest {
journal_bytes: None,
target_timestamp: None,
expire_timestamp: None,
gas_estimate: None,
expected_reward_eth: None,
cached_id: OnceLock::new(),
}
Expand Down Expand Up @@ -717,6 +721,10 @@ pub trait OrderPricingContext {
.await?,
)
};
// Store the gas estimate on the order so the pre-lock check can re-use it
// instead of re-computing.
order.gas_estimate = Some(order_gas.saturating_to::<u64>());

let mut order_gas_cost = U256::from(gas_price) * order_gas;
tracing::debug!(
"Estimated {order_gas} gas to {} order {order_id}; {} ether @ {} gwei",
Expand Down
112 changes: 90 additions & 22 deletions crates/broker/src/order_monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ pub enum OrderMonitorErr {

#[error("{code} Unexpected error: {0:?}", code = self.code())]
UnexpectedError(#[from] anyhow::Error),

/// Pre-lock gas check failed (e.g. gas spike). Caller may retry next block.
#[error("{code} Pre-lock gas check failed (retry later): {0}", code = self.code())]
PreLockCheckRetry(String),
}

impl_coded_debug!(OrderMonitorErr);
Expand All @@ -83,6 +87,7 @@ impl CodedError for OrderMonitorErr {
OrderMonitorErr::AlreadyLocked => "[B-OM-009]",
OrderMonitorErr::InsufficientBalance => "[B-OM-010]",
OrderMonitorErr::RpcErr(_) => "[B-OM-011]",
OrderMonitorErr::PreLockCheckRetry(_) => "[B-OM-012]",
OrderMonitorErr::UnexpectedError(_) => "[B-OM-500]",
}
}
Expand Down Expand Up @@ -283,6 +288,32 @@ where
request_id,
order.request.offer.lockCollateral
);

// Pre-lock gas profitability check: compare gas cost against the order's current
// reward on the pricing ramp. Retries next block on failure (e.g. gas spike).
if let Some(gas_estimate) = order.gas_estimate {
let gas_price = self
.chain_monitor
.current_gas_price()
.await
.map_err(|e| OrderMonitorErr::PreLockCheckRetry(format!("gas price: {e:#}")))?;
let gas_cost = U256::from(gas_price) * U256::from(gas_estimate);
let reward = order
.request
.offer
.price_at(now_timestamp())
.map_err(|e| OrderMonitorErr::PreLockCheckRetry(e.to_string()))?;
if gas_cost > reward {
let msg = format!(
"gas cost {} exceeds reward {}",
format_ether(gas_cost),
format_ether(reward)
);
tracing::warn!("Pre-lock check failed for request 0x{:x}: {msg}", request_id);
return Err(OrderMonitorErr::PreLockCheckRetry(msg));
}
}

let lock_block =
self.market.lock_request(&order.request, order.client_sig.clone()).await.map_err(
|e| -> OrderMonitorErr {
Expand Down Expand Up @@ -567,6 +598,7 @@ where
}

let request_id = order.request.id;
let mut should_invalidate = true;
match self.lock_order(order).await {
Ok(lock_price) => {
tracing::info!("Locked request: 0x{:x}", request_id);
Expand All @@ -579,32 +611,24 @@ where
}
}
Err(ref err) => {
match err {
OrderMonitorErr::UnexpectedError(inner) => {
tracing::error!(
"Failed to lock order: {order_id} - {} - {inner:?}",
err.code()
);
}
OrderMonitorErr::AlreadyLocked => {
// For order already locked, we don't need to print the error backtrace.
tracing::warn!("Soft failed to lock request: {order_id} - {}", err.code());
if let OrderMonitorErr::PreLockCheckRetry(reason) = err {
tracing::warn!("Pre-lock check failed for {order_id}: {reason}, will retry next block");
should_invalidate = false;
} else {
if matches!(err, OrderMonitorErr::UnexpectedError(_)) {
tracing::error!("Failed to lock order: {order_id} - {err:?}");
} else {
tracing::warn!("Failed to lock order: {order_id} - {err:?}");
}
_ => {
tracing::warn!(
"Soft failed to lock request: {order_id} - {} - {err:?}",
err.code()
);
if let Err(e) = self.db.insert_skipped_request(order).await {
tracing::error!("Failed to set DB failure state for order: {order_id} - {e:?}");
}
}
if let Err(err) = self.db.insert_skipped_request(order).await {
tracing::error!(
"Failed to set DB failure state for order: {order_id} - {err:?}"
);
}
}
}
self.lock_and_prove_cache.invalidate(&order_id).await;
if should_invalidate {
self.lock_and_prove_cache.invalidate(&order_id).await;
}
} else {
if self.listen_only {
tracing::info!(
Expand All @@ -630,6 +654,16 @@ where
Ok(())
}

#[cfg(test)]
pub(crate) async fn test_insert_into_lock_cache(&self, order: Arc<OrderRequest>) {
self.lock_and_prove_cache.insert(order.id().clone(), order).await;
}

#[cfg(test)]
pub(crate) async fn test_lock_cache_contains(&self, order_id: &str) -> bool {
self.lock_and_prove_cache.get(order_id).await.is_some()
}

/// Calculate the gas units needed for an order and the corresponding cost in wei
async fn calculate_order_gas_cost_wei(
&self,
Expand Down Expand Up @@ -1101,7 +1135,6 @@ pub(crate) mod tests {
pub anvil: AnvilInstance,
pub db: DbObj,
pub market_address: Address,
#[allow(dead_code)]
pub config: ConfigLock,
pub priced_order_tx: mpsc::Sender<Box<OrderRequest>>,
pub signer: PrivateKeySigner,
Expand Down Expand Up @@ -1917,4 +1950,39 @@ pub(crate) mod tests {
"Expected log message about insufficient collateral balance"
);
}

/// Test that when gas cost exceeds the order's reward on the pricing ramp, the inline
/// pre-lock gas check keeps the order in cache for retry on the next block.
/// The test order's tiny maxPrice (2 wei) is far below Anvil's gas cost, so the check
/// naturally fails.
#[tokio::test]
#[traced_test]
async fn pre_lock_check_gas_too_high_keeps_order_in_cache() {
let mut ctx = setup_om_test_context().await;

// Use now_timestamp() for rampUpStart so expires_at() is in the future.
let mut order =
ctx.create_test_order(FulfillmentType::LockAndFulfill, now_timestamp(), 100, 200).await;
// Set gas_estimate so the inline pre-lock check fires.
// lockin_gas_estimate (200k) + fulfill_gas_estimate (300k) = 500k gas.
// At Anvil's default gas price (~1 gwei), cost = 500k * 1e9 = 5e14 wei,
// which far exceeds the order's reward of 2 wei.
order.gas_estimate = Some(500_000);
// Submit request on-chain so lock_order's get_status sees it as open (Unknown).
let _ = ctx.market_service.submit_request(&order.request, &ctx.signer).await.unwrap();
let order_arc = Arc::new(*order);
let order_id = order_arc.id().clone();
ctx.monitor.test_insert_into_lock_cache(order_arc.clone()).await;

let valid = ctx.monitor.get_valid_orders(1, 0).await.unwrap();
assert_eq!(valid.len(), 1, "expect single order from cache");
assert_eq!(valid[0].id(), order_id, "valid order must be the one we inserted");
ctx.monitor.lock_and_prove_orders(&valid).await.unwrap();

assert!(
ctx.monitor.test_lock_cache_contains(&order_id).await,
"When gas cost exceeds reward, order stays in cache and will retry next block"
);
assert!(logs_contain("gas cost"), "Expected log about gas cost exceeding reward");
}
}
Loading