From df56096d4376b1c8a50f888e1ac79d9717903ba0 Mon Sep 17 00:00:00 2001 From: William Law Date: Tue, 2 Dec 2025 14:46:12 -0500 Subject: [PATCH 1/3] dont err if we cant simulate --- crates/ingress-rpc/src/service.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/ingress-rpc/src/service.rs b/crates/ingress-rpc/src/service.rs index 0755e35..85d866f 100644 --- a/crates/ingress-rpc/src/service.rs +++ b/crates/ingress-rpc/src/service.rs @@ -1,6 +1,6 @@ use alloy_consensus::transaction::Recovered; use alloy_consensus::{Transaction, transaction::SignerRecoverable}; -use alloy_primitives::{B256, Bytes}; +use alloy_primitives::{B256, Bytes, U256}; use alloy_provider::{Provider, RootProvider, network::eip2718::Decodable2718}; use jsonrpsee::{ core::{RpcResult, async_trait}, @@ -214,7 +214,22 @@ where .map_err(|e: String| EthApiError::InvalidParams(e).into_rpc_err())?; let bundle_hash = &parsed_bundle.bundle_hash(); - let meter_bundle_response = self.meter_bundle(&bundle, bundle_hash).await?; + let meter_bundle_response = self.meter_bundle(&bundle, bundle_hash).await + .unwrap_or_else(|_| { + warn!(message = "Bundle simulation failed, using default response", bundle_hash = %bundle_hash); + MeterBundleResponse { + bundle_gas_price: U256::from(0), + bundle_hash: *bundle_hash, + coinbase_diff: U256::from(0), + eth_sent_to_coinbase: U256::from(0), + gas_fees: U256::from(0), + results: vec![], + state_block_number: 0, + state_flashblock_index: None, + total_gas_used: 0, + total_execution_time_us: 0, + } + }); let accepted_bundle = AcceptedBundle::new(parsed_bundle, meter_bundle_response.clone()); From d01906500c60ab6f89aed3e12e5e9b247c49dd6f Mon Sep 17 00:00:00 2001 From: William Law Date: Tue, 2 Dec 2025 14:52:53 -0500 Subject: [PATCH 2/3] use Default trait --- crates/core/src/types.rs | 2 +- crates/ingress-rpc/src/service.rs | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 35fd522..ae00f36 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -278,7 +278,7 @@ pub struct TransactionResult { pub execution_time_us: u128, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(rename_all = "camelCase")] pub struct MeterBundleResponse { pub bundle_gas_price: U256, diff --git a/crates/ingress-rpc/src/service.rs b/crates/ingress-rpc/src/service.rs index 85d866f..a9523ec 100644 --- a/crates/ingress-rpc/src/service.rs +++ b/crates/ingress-rpc/src/service.rs @@ -1,6 +1,6 @@ use alloy_consensus::transaction::Recovered; use alloy_consensus::{Transaction, transaction::SignerRecoverable}; -use alloy_primitives::{B256, Bytes, U256}; +use alloy_primitives::{B256, Bytes}; use alloy_provider::{Provider, RootProvider, network::eip2718::Decodable2718}; use jsonrpsee::{ core::{RpcResult, async_trait}, @@ -216,19 +216,9 @@ where let bundle_hash = &parsed_bundle.bundle_hash(); let meter_bundle_response = self.meter_bundle(&bundle, bundle_hash).await .unwrap_or_else(|_| { + // TODO: in the future, we should return the error warn!(message = "Bundle simulation failed, using default response", bundle_hash = %bundle_hash); - MeterBundleResponse { - bundle_gas_price: U256::from(0), - bundle_hash: *bundle_hash, - coinbase_diff: U256::from(0), - eth_sent_to_coinbase: U256::from(0), - gas_fees: U256::from(0), - results: vec![], - state_block_number: 0, - state_flashblock_index: None, - total_gas_used: 0, - total_execution_time_us: 0, - } + MeterBundleResponse::default() }); let accepted_bundle = AcceptedBundle::new(parsed_bundle, meter_bundle_response.clone()); From b6ce707875ff602dc6c45dcc083416355a305072 Mon Sep 17 00:00:00 2001 From: William Law Date: Wed, 3 Dec 2025 10:09:56 -0500 Subject: [PATCH 3/3] add unit test --- Cargo.lock | 1 + crates/ingress-rpc/Cargo.toml | 4 ++ crates/ingress-rpc/src/service.rs | 96 +++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 2db75e8..dc5c850 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7114,6 +7114,7 @@ dependencies = [ "tokio", "tracing", "url", + "wiremock", ] [[package]] diff --git a/crates/ingress-rpc/Cargo.toml b/crates/ingress-rpc/Cargo.toml index 462ef28..e1e460f 100644 --- a/crates/ingress-rpc/Cargo.toml +++ b/crates/ingress-rpc/Cargo.toml @@ -40,3 +40,7 @@ metrics.workspace = true metrics-derive.workspace = true metrics-exporter-prometheus.workspace = true axum.workspace = true + +[dev-dependencies] +wiremock.workspace = true +serde_json.workspace = true diff --git a/crates/ingress-rpc/src/service.rs b/crates/ingress-rpc/src/service.rs index a9523ec..a883636 100644 --- a/crates/ingress-rpc/src/service.rs +++ b/crates/ingress-rpc/src/service.rs @@ -403,7 +403,54 @@ where #[cfg(test)] mod tests { use super::*; + use crate::{Config, TxSubmissionMethod, queue::QueuePublisher}; + use alloy_provider::RootProvider; + use async_trait::async_trait; + use std::net::{IpAddr, SocketAddr}; + use std::sync::Arc; use tips_core::test_utils::create_test_meter_bundle_response; + use tokio::sync::{broadcast, mpsc}; + use url::Url; + use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method}; + + struct MockQueue; + + #[async_trait] + impl QueuePublisher for MockQueue { + async fn publish( + &self, + _bundle: &tips_core::AcceptedBundle, + _bundle_hash: &B256, + ) -> anyhow::Result<()> { + Ok(()) + } + } + + fn create_test_config(mock_server: &MockServer) -> Config { + Config { + address: IpAddr::from([127, 0, 0, 1]), + port: 8080, + mempool_url: Url::parse("http://localhost:3000").unwrap(), + tx_submission_method: TxSubmissionMethod::Mempool, + ingress_kafka_properties: String::new(), + ingress_topic: String::new(), + audit_kafka_properties: String::new(), + audit_topic: String::new(), + log_level: String::from("info"), + log_format: tips_core::logger::LogFormat::Pretty, + send_transaction_default_lifetime_seconds: 300, + simulation_rpc: mock_server.uri().parse().unwrap(), + metrics_addr: SocketAddr::from(([127, 0, 0, 1], 9002)), + block_time_milliseconds: 1000, + meter_bundle_timeout_ms: 5000, + validate_user_operation_timeout_ms: 2000, + builder_rpcs: vec![], + max_buffered_meter_bundle_responses: 100, + max_buffered_backrun_bundles: 100, + health_check_addr: SocketAddr::from(([127, 0, 0, 1], 8081)), + backrun_enabled: false, + } + } #[tokio::test] async fn test_timeout_logic() { @@ -449,4 +496,53 @@ mod tests { let res = result.unwrap().unwrap(); assert_eq!(res, create_test_meter_bundle_response()); } + + // Replicate a failed `meter_bundle` request and instead of returning an error, we return a default `MeterBundleResponse` + #[tokio::test] + async fn test_meter_bundle_success() { + let mock_server = MockServer::start().await; + + // Mock error response from base_meterBundle + Mock::given(method("POST")) + .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32000, + "message": "Simulation failed" + } + }))) + .mount(&mock_server) + .await; + + let config = create_test_config(&mock_server); + + let provider: RootProvider = + RootProvider::new_http(mock_server.uri().parse().unwrap()); + let simulation_provider = Arc::new(provider.clone()); + + let (audit_tx, _audit_rx) = mpsc::unbounded_channel(); + let (builder_tx, _builder_rx) = broadcast::channel(1); + let (backrun_tx, _backrun_rx) = broadcast::channel(1); + + let service = IngressService::new( + provider, + simulation_provider.as_ref().clone(), + MockQueue, + audit_tx, + builder_tx, + backrun_tx, + config, + ); + + let bundle = Bundle::default(); + let bundle_hash = B256::default(); + + let result = service.meter_bundle(&bundle, &bundle_hash).await; + + // Test that meter_bundle returns an error, but we handle it gracefully + assert!(result.is_err()); + let response = result.unwrap_or_else(|_| MeterBundleResponse::default()); + assert_eq!(response, MeterBundleResponse::default()); + } }