Skip to content

Commit 857899b

Browse files
authored
feat: simulate bundles in ingress-rpc (#46)
* spike * use tips-core * comments + add meterbundleres to bundlewmetadata * make meter_response not optional * make it a fn not default
1 parent 4079b16 commit 857899b

File tree

5 files changed

+89
-30
lines changed

5 files changed

+89
-30
lines changed

crates/core/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ pub mod types;
55
#[cfg(any(test, feature = "test-utils"))]
66
pub mod test_utils;
77

8-
pub use types::{Bundle, BundleHash, BundleWithMetadata, CancelBundle};
8+
pub use types::{
9+
BLOCK_TIME, Bundle, BundleHash, BundleWithMetadata, CancelBundle, MeterBundleResponse,
10+
};

crates/core/src/test_utils.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use crate::{Bundle, BundleWithMetadata};
1+
use crate::{Bundle, BundleWithMetadata, MeterBundleResponse};
22
use alloy_consensus::SignableTransaction;
3-
use alloy_primitives::{Address, U256};
3+
use alloy_primitives::{Address, B256, U256};
44
use alloy_provider::network::TxSignerSync;
55
use alloy_provider::network::eip2718::Encodable2718;
66
use alloy_signer_local::PrivateKeySigner;
@@ -38,6 +38,23 @@ pub fn create_test_bundle(
3838
max_timestamp,
3939
..Default::default()
4040
};
41+
let meter_bundle_response = create_test_meter_bundle_response();
4142

42-
BundleWithMetadata::load(bundle).unwrap()
43+
BundleWithMetadata::load(bundle, meter_bundle_response).unwrap()
44+
}
45+
46+
pub fn create_test_meter_bundle_response() -> MeterBundleResponse {
47+
MeterBundleResponse {
48+
bundle_gas_price: "0".to_string(),
49+
bundle_hash: B256::default(),
50+
coinbase_diff: "0".to_string(),
51+
eth_sent_to_coinbase: "0".to_string(),
52+
gas_fees: "0".to_string(),
53+
results: vec![],
54+
state_block_number: 0,
55+
state_flashblock_index: None,
56+
total_gas_used: 0,
57+
total_execution_time_us: 0,
58+
state_root_time_us: 0,
59+
}
4360
}

crates/core/src/types.rs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ use op_alloy_flz::tx_estimated_size_fjord_bytes;
77
use serde::{Deserialize, Serialize};
88
use uuid::Uuid;
99

10+
/// Block time in microseconds
11+
pub const BLOCK_TIME: u128 = 2_000_000;
12+
1013
#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1114
#[serde(rename_all = "camelCase")]
1215
pub struct Bundle {
@@ -70,10 +73,14 @@ pub struct BundleWithMetadata {
7073
bundle: Bundle,
7174
uuid: Uuid,
7275
transactions: Vec<OpTxEnvelope>,
76+
meter_bundle_response: MeterBundleResponse,
7377
}
7478

7579
impl BundleWithMetadata {
76-
pub fn load(mut bundle: Bundle) -> Result<Self, String> {
80+
pub fn load(
81+
mut bundle: Bundle,
82+
meter_bundle_response: MeterBundleResponse,
83+
) -> Result<Self, String> {
7784
let uuid = bundle
7885
.replacement_uuid
7986
.clone()
@@ -96,6 +103,7 @@ impl BundleWithMetadata {
96103
bundle,
97104
transactions,
98105
uuid,
106+
meter_bundle_response,
99107
})
100108
}
101109

@@ -181,7 +189,7 @@ pub struct MeterBundleResponse {
181189
#[cfg(test)]
182190
mod tests {
183191
use super::*;
184-
use crate::test_utils::create_transaction;
192+
use crate::test_utils::{create_test_meter_bundle_response, create_transaction};
185193
use alloy_primitives::Keccak256;
186194
use alloy_provider::network::eip2718::Encodable2718;
187195
use alloy_signer_local::PrivateKeySigner;
@@ -197,12 +205,15 @@ mod tests {
197205
let tx1_bytes = tx1.encoded_2718();
198206
let tx2_bytes = tx2.encoded_2718();
199207

200-
let bundle = BundleWithMetadata::load(Bundle {
201-
replacement_uuid: None,
202-
txs: vec![tx1_bytes.clone().into()],
203-
block_number: 1,
204-
..Default::default()
205-
})
208+
let bundle = BundleWithMetadata::load(
209+
Bundle {
210+
replacement_uuid: None,
211+
txs: vec![tx1_bytes.clone().into()],
212+
block_number: 1,
213+
..Default::default()
214+
},
215+
create_test_meter_bundle_response(),
216+
)
206217
.unwrap();
207218

208219
assert!(!bundle.uuid().is_nil());
@@ -225,12 +236,15 @@ mod tests {
225236
assert_eq!(bundle.bundle_hash(), expected_bundle_hash_single);
226237

227238
let uuid = Uuid::new_v4();
228-
let bundle = BundleWithMetadata::load(Bundle {
229-
replacement_uuid: Some(uuid.to_string()),
230-
txs: vec![tx1_bytes.clone().into(), tx2_bytes.clone().into()],
231-
block_number: 1,
232-
..Default::default()
233-
})
239+
let bundle = BundleWithMetadata::load(
240+
Bundle {
241+
replacement_uuid: Some(uuid.to_string()),
242+
txs: vec![tx1_bytes.clone().into(), tx2_bytes.clone().into()],
243+
block_number: 1,
244+
..Default::default()
245+
},
246+
create_test_meter_bundle_response(),
247+
)
234248
.unwrap();
235249

236250
assert_eq!(*bundle.uuid(), uuid);

crates/ingress-rpc/src/queue.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ impl QueuePublisher for KafkaQueuePublisher {
7575
mod tests {
7676
use super::*;
7777
use rdkafka::config::ClientConfig;
78-
use tips_core::{Bundle, BundleWithMetadata};
78+
use tips_core::{Bundle, BundleWithMetadata, test_utils::create_test_meter_bundle_response};
7979
use tokio::time::{Duration, Instant};
8080

8181
fn create_test_bundle() -> Bundle {
@@ -93,7 +93,8 @@ mod tests {
9393

9494
let publisher = KafkaQueuePublisher::new(producer, "tips-ingress-rpc".to_string());
9595
let bundle = create_test_bundle();
96-
let bundle_with_metadata = BundleWithMetadata::load(bundle.clone()).unwrap();
96+
let bundle_with_metadata =
97+
BundleWithMetadata::load(bundle.clone(), create_test_meter_bundle_response()).unwrap();
9798
let bundle_hash = bundle_with_metadata.bundle_hash();
9899

99100
let start = Instant::now();

crates/ingress-rpc/src/service.rs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use op_alloy_network::Optimism;
1111
use reth_rpc_eth_types::EthApiError;
1212
use std::time::{SystemTime, UNIX_EPOCH};
1313
use tips_audit::{BundleEvent, BundleEventPublisher};
14-
use tips_core::{Bundle, BundleHash, BundleWithMetadata, CancelBundle};
14+
use tips_core::{
15+
BLOCK_TIME, Bundle, BundleHash, BundleWithMetadata, CancelBundle, MeterBundleResponse,
16+
};
1517
use tracing::{info, warn};
1618

1719
use crate::queue::QueuePublisher;
@@ -65,7 +67,10 @@ where
6567
Audit: BundleEventPublisher + Sync + Send + 'static,
6668
{
6769
async fn send_bundle(&self, bundle: Bundle) -> RpcResult<BundleHash> {
68-
let bundle_with_metadata = self.validate_bundle(bundle).await?;
70+
self.validate_bundle(&bundle).await?;
71+
let meter_bundle_response = self.meter_bundle(&bundle).await?;
72+
let bundle_with_metadata = BundleWithMetadata::load(bundle, meter_bundle_response)
73+
.map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?;
6974

7075
let bundle_hash = bundle_with_metadata.bundle_hash();
7176
if let Err(e) = self
@@ -117,8 +122,9 @@ where
117122
reverting_tx_hashes: vec![transaction.tx_hash()],
118123
..Default::default()
119124
};
125+
let meter_bundle_response = self.meter_bundle(&bundle).await?;
120126

121-
let bundle_with_metadata = BundleWithMetadata::load(bundle)
127+
let bundle_with_metadata = BundleWithMetadata::load(bundle, meter_bundle_response)
122128
.map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?;
123129
let bundle_hash = bundle_with_metadata.bundle_hash();
124130

@@ -191,25 +197,44 @@ where
191197
Ok(transaction)
192198
}
193199

194-
async fn validate_bundle(&self, bundle: Bundle) -> RpcResult<BundleWithMetadata> {
200+
async fn validate_bundle(&self, bundle: &Bundle) -> RpcResult<()> {
195201
if bundle.txs.is_empty() {
196202
return Err(
197203
EthApiError::InvalidParams("Bundle cannot have empty transactions".into())
198204
.into_rpc_err(),
199205
);
200206
}
201207

202-
let bundle_with_metadata = BundleWithMetadata::load(bundle.clone())
203-
.map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?;
204-
let tx_hashes = bundle_with_metadata.txn_hashes();
205-
206208
let mut total_gas = 0u64;
209+
let mut tx_hashes = Vec::new();
207210
for tx_data in &bundle.txs {
208211
let transaction = self.validate_tx(tx_data).await?;
209212
total_gas = total_gas.saturating_add(transaction.gas_limit());
213+
tx_hashes.push(transaction.tx_hash());
210214
}
211-
validate_bundle(&bundle, total_gas, tx_hashes)?;
215+
validate_bundle(bundle, total_gas, tx_hashes)?;
212216

213-
Ok(bundle_with_metadata)
217+
Ok(())
218+
}
219+
220+
/// `meter_bundle` is used to determine how long a bundle will take to execute. A bundle that
221+
/// is within `BLOCK_TIME` will return the `MeterBundleResponse` that can be passed along
222+
/// to the builder.
223+
async fn meter_bundle(&self, bundle: &Bundle) -> RpcResult<MeterBundleResponse> {
224+
let res: MeterBundleResponse = self
225+
.provider
226+
.client()
227+
.request("base_meterBundle", (bundle,))
228+
.await
229+
.map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?;
230+
231+
// we can save some builder payload building computation by not including bundles
232+
// that we know will take longer than the block time to execute
233+
if res.total_execution_time_us > BLOCK_TIME {
234+
return Err(
235+
EthApiError::InvalidParams("Bundle simulation took too long".into()).into_rpc_err(),
236+
);
237+
}
238+
Ok(res)
214239
}
215240
}

0 commit comments

Comments
 (0)