Skip to content
Merged
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
4 changes: 3 additions & 1 deletion crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
23 changes: 20 additions & 3 deletions crates/core/src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
}
}
42 changes: 28 additions & 14 deletions crates/core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -70,10 +73,14 @@ pub struct BundleWithMetadata {
bundle: Bundle,
uuid: Uuid,
transactions: Vec<OpTxEnvelope>,
meter_bundle_response: MeterBundleResponse,
}

impl BundleWithMetadata {
pub fn load(mut bundle: Bundle) -> Result<Self, String> {
pub fn load(
mut bundle: Bundle,
meter_bundle_response: MeterBundleResponse,
) -> Result<Self, String> {
let uuid = bundle
.replacement_uuid
.clone()
Expand All @@ -96,6 +103,7 @@ impl BundleWithMetadata {
bundle,
transactions,
uuid,
meter_bundle_response,
})
}

Expand Down Expand Up @@ -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;
Expand All @@ -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());
Expand All @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions crates/ingress-rpc/src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
Expand Down
45 changes: 35 additions & 10 deletions crates/ingress-rpc/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,7 +67,10 @@ where
Audit: BundleEventPublisher + Sync + Send + 'static,
{
async fn send_bundle(&self, bundle: Bundle) -> RpcResult<BundleHash> {
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
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -191,25 +197,44 @@ where
Ok(transaction)
}

async fn validate_bundle(&self, bundle: Bundle) -> RpcResult<BundleWithMetadata> {
async fn validate_bundle(&self, bundle: &Bundle) -> RpcResult<()> {
if bundle.txs.is_empty() {
return Err(
EthApiError::InvalidParams("Bundle cannot have empty transactions".into())
.into_rpc_err(),
);
}

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<MeterBundleResponse> {
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)
}
}