diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 0b019008..5b7906cd 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -5,4 +5,6 @@ pub mod types; #[cfg(any(test, feature = "test-utils"))] pub mod test_utils; -pub use types::{Bundle, BundleHash, BundleWithMetadata, CancelBundle}; +pub use types::{ + BLOCK_TIME, Bundle, BundleHash, BundleWithMetadata, CancelBundle, MeterBundleResponse, +}; diff --git a/crates/core/src/test_utils.rs b/crates/core/src/test_utils.rs index a5763548..cb2c5ee6 100644 --- a/crates/core/src/test_utils.rs +++ b/crates/core/src/test_utils.rs @@ -1,6 +1,6 @@ -use crate::{Bundle, BundleWithMetadata}; +use crate::{Bundle, BundleWithMetadata, MeterBundleResponse}; use alloy_consensus::SignableTransaction; -use alloy_primitives::{Address, U256}; +use alloy_primitives::{Address, B256, U256}; use alloy_provider::network::TxSignerSync; use alloy_provider::network::eip2718::Encodable2718; use alloy_signer_local::PrivateKeySigner; @@ -38,6 +38,23 @@ pub fn create_test_bundle( max_timestamp, ..Default::default() }; + let meter_bundle_response = create_test_meter_bundle_response(); - BundleWithMetadata::load(bundle).unwrap() + BundleWithMetadata::load(bundle, meter_bundle_response).unwrap() +} + +pub fn create_test_meter_bundle_response() -> MeterBundleResponse { + MeterBundleResponse { + bundle_gas_price: "0".to_string(), + bundle_hash: B256::default(), + coinbase_diff: "0".to_string(), + eth_sent_to_coinbase: "0".to_string(), + gas_fees: "0".to_string(), + results: vec![], + state_block_number: 0, + state_flashblock_index: None, + total_gas_used: 0, + total_execution_time_us: 0, + state_root_time_us: 0, + } } diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index f79a5ab0..97ca1684 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -7,6 +7,9 @@ use op_alloy_flz::tx_estimated_size_fjord_bytes; use serde::{Deserialize, Serialize}; use uuid::Uuid; +/// Block time in microseconds +pub const BLOCK_TIME: u128 = 2_000_000; + #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Bundle { @@ -70,10 +73,14 @@ pub struct BundleWithMetadata { bundle: Bundle, uuid: Uuid, transactions: Vec, + meter_bundle_response: MeterBundleResponse, } impl BundleWithMetadata { - pub fn load(mut bundle: Bundle) -> Result { + pub fn load( + mut bundle: Bundle, + meter_bundle_response: MeterBundleResponse, + ) -> Result { let uuid = bundle .replacement_uuid .clone() @@ -96,6 +103,7 @@ impl BundleWithMetadata { bundle, transactions, uuid, + meter_bundle_response, }) } @@ -181,7 +189,7 @@ pub struct MeterBundleResponse { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::create_transaction; + use crate::test_utils::{create_test_meter_bundle_response, create_transaction}; use alloy_primitives::Keccak256; use alloy_provider::network::eip2718::Encodable2718; use alloy_signer_local::PrivateKeySigner; @@ -197,12 +205,15 @@ mod tests { let tx1_bytes = tx1.encoded_2718(); let tx2_bytes = tx2.encoded_2718(); - let bundle = BundleWithMetadata::load(Bundle { - replacement_uuid: None, - txs: vec![tx1_bytes.clone().into()], - block_number: 1, - ..Default::default() - }) + let bundle = BundleWithMetadata::load( + Bundle { + replacement_uuid: None, + txs: vec![tx1_bytes.clone().into()], + block_number: 1, + ..Default::default() + }, + create_test_meter_bundle_response(), + ) .unwrap(); assert!(!bundle.uuid().is_nil()); @@ -225,12 +236,15 @@ mod tests { assert_eq!(bundle.bundle_hash(), expected_bundle_hash_single); let uuid = Uuid::new_v4(); - let bundle = BundleWithMetadata::load(Bundle { - replacement_uuid: Some(uuid.to_string()), - txs: vec![tx1_bytes.clone().into(), tx2_bytes.clone().into()], - block_number: 1, - ..Default::default() - }) + let bundle = BundleWithMetadata::load( + Bundle { + replacement_uuid: Some(uuid.to_string()), + txs: vec![tx1_bytes.clone().into(), tx2_bytes.clone().into()], + block_number: 1, + ..Default::default() + }, + create_test_meter_bundle_response(), + ) .unwrap(); assert_eq!(*bundle.uuid(), uuid); diff --git a/crates/ingress-rpc/src/queue.rs b/crates/ingress-rpc/src/queue.rs index 28a70932..0f605208 100644 --- a/crates/ingress-rpc/src/queue.rs +++ b/crates/ingress-rpc/src/queue.rs @@ -75,7 +75,7 @@ impl QueuePublisher for KafkaQueuePublisher { mod tests { use super::*; use rdkafka::config::ClientConfig; - use tips_core::{Bundle, BundleWithMetadata}; + use tips_core::{Bundle, BundleWithMetadata, test_utils::create_test_meter_bundle_response}; use tokio::time::{Duration, Instant}; fn create_test_bundle() -> Bundle { @@ -93,7 +93,8 @@ mod tests { let publisher = KafkaQueuePublisher::new(producer, "tips-ingress-rpc".to_string()); let bundle = create_test_bundle(); - let bundle_with_metadata = BundleWithMetadata::load(bundle.clone()).unwrap(); + let bundle_with_metadata = + BundleWithMetadata::load(bundle.clone(), create_test_meter_bundle_response()).unwrap(); let bundle_hash = bundle_with_metadata.bundle_hash(); let start = Instant::now(); diff --git a/crates/ingress-rpc/src/service.rs b/crates/ingress-rpc/src/service.rs index 3ea04081..db38b867 100644 --- a/crates/ingress-rpc/src/service.rs +++ b/crates/ingress-rpc/src/service.rs @@ -11,7 +11,9 @@ use op_alloy_network::Optimism; use reth_rpc_eth_types::EthApiError; use std::time::{SystemTime, UNIX_EPOCH}; use tips_audit::{BundleEvent, BundleEventPublisher}; -use tips_core::{Bundle, BundleHash, BundleWithMetadata, CancelBundle}; +use tips_core::{ + BLOCK_TIME, Bundle, BundleHash, BundleWithMetadata, CancelBundle, MeterBundleResponse, +}; use tracing::{info, warn}; use crate::queue::QueuePublisher; @@ -65,7 +67,10 @@ where Audit: BundleEventPublisher + Sync + Send + 'static, { async fn send_bundle(&self, bundle: Bundle) -> RpcResult { - let bundle_with_metadata = self.validate_bundle(bundle).await?; + self.validate_bundle(&bundle).await?; + let meter_bundle_response = self.meter_bundle(&bundle).await?; + let bundle_with_metadata = BundleWithMetadata::load(bundle, meter_bundle_response) + .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; let bundle_hash = bundle_with_metadata.bundle_hash(); if let Err(e) = self @@ -117,8 +122,9 @@ where reverting_tx_hashes: vec![transaction.tx_hash()], ..Default::default() }; + let meter_bundle_response = self.meter_bundle(&bundle).await?; - let bundle_with_metadata = BundleWithMetadata::load(bundle) + let bundle_with_metadata = BundleWithMetadata::load(bundle, meter_bundle_response) .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; let bundle_hash = bundle_with_metadata.bundle_hash(); @@ -191,7 +197,7 @@ where Ok(transaction) } - async fn validate_bundle(&self, bundle: Bundle) -> RpcResult { + async fn validate_bundle(&self, bundle: &Bundle) -> RpcResult<()> { if bundle.txs.is_empty() { return Err( EthApiError::InvalidParams("Bundle cannot have empty transactions".into()) @@ -199,17 +205,36 @@ where ); } - let bundle_with_metadata = BundleWithMetadata::load(bundle.clone()) - .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; - let tx_hashes = bundle_with_metadata.txn_hashes(); - let mut total_gas = 0u64; + let mut tx_hashes = Vec::new(); for tx_data in &bundle.txs { let transaction = self.validate_tx(tx_data).await?; total_gas = total_gas.saturating_add(transaction.gas_limit()); + tx_hashes.push(transaction.tx_hash()); } - validate_bundle(&bundle, total_gas, tx_hashes)?; + validate_bundle(bundle, total_gas, tx_hashes)?; - Ok(bundle_with_metadata) + Ok(()) + } + + /// `meter_bundle` is used to determine how long a bundle will take to execute. A bundle that + /// is within `BLOCK_TIME` will return the `MeterBundleResponse` that can be passed along + /// to the builder. + async fn meter_bundle(&self, bundle: &Bundle) -> RpcResult { + let res: MeterBundleResponse = self + .provider + .client() + .request("base_meterBundle", (bundle,)) + .await + .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; + + // we can save some builder payload building computation by not including bundles + // that we know will take longer than the block time to execute + if res.total_execution_time_us > BLOCK_TIME { + return Err( + EthApiError::InvalidParams("Bundle simulation took too long".into()).into_rpc_err(), + ); + } + Ok(res) } }