1use alloy_evm::EvmEnv;
2use alloy_primitives::U256;
3#[cfg(not(debug_assertions))]
4use chrono::TimeDelta;
5use eyre::{Result, eyre};
6use influxdb::{Timestamp, WriteQuery};
7use kabu_core_components::Component;
8use rayon::prelude::*;
9use rayon::{ThreadPool, ThreadPoolBuilder};
10use reth_tasks::TaskExecutor;
11use revm::context::{BlockEnv, CfgEnv};
12use revm::context_interface::block::BlobExcessGasAndPrice;
13use revm::{Database, DatabaseCommit, DatabaseRef};
14use std::collections::{BTreeMap, HashSet};
15use std::sync::Arc;
16use tokio::sync::broadcast::error::RecvError;
17use tokio::sync::{RwLock, broadcast};
18#[cfg(not(debug_assertions))]
19use tracing::warn;
20use tracing::{debug, error, info};
21
22use crate::{BackrunConfig, SwapCalculator};
23
24use kabu_evm_db::{DatabaseHelpers, KabuDBError};
25use kabu_types_entities::strategy_config::StrategyConfig;
26use kabu_types_events::{
27 BestTxSwapCompose, HealthEvent, Message, MessageHealthEvent, MessageSwapCompose, StateUpdateEvent, SwapComposeData, SwapComposeMessage,
28 TxComposeData,
29};
30use kabu_types_market::{Market, PoolWrapper, SwapDirection, SwapPath};
31use kabu_types_swap::{Swap, SwapError, SwapLine};
32use reth_node_types::NodePrimitives;
33
34async fn state_change_arb_searcher_task<
35 DB: DatabaseRef<Error = KabuDBError> + Database<Error = KabuDBError> + DatabaseCommit + Send + Sync + Clone + Default + 'static,
36 LDT: NodePrimitives,
37>(
38 thread_pool: Arc<ThreadPool>,
39 backrun_config: BackrunConfig,
40 state_update_event: StateUpdateEvent<DB, LDT>,
41 market: Arc<RwLock<Market>>,
42 swap_request_tx: broadcast::Sender<MessageSwapCompose<DB, LDT>>,
43 pool_health_monitor_tx: broadcast::Sender<MessageHealthEvent>,
44 influxdb_write_channel_tx: Option<broadcast::Sender<WriteQuery>>,
45) -> Result<()> {
46 debug!("Message received {} stuffing : {:?}", state_update_event.origin, state_update_event.stuffing_tx_hash());
47
48 let mut db = state_update_event.market_state().clone();
49 DatabaseHelpers::apply_geth_state_update_vec(&mut db, state_update_event.state_update().clone());
50
51 let start_time_utc = chrono::Utc::now();
52
53 let start_time = std::time::Instant::now();
54 #[allow(clippy::mutable_key_type)]
55 let mut swap_path_set: HashSet<SwapPath> = HashSet::new();
56
57 let market_guard_read = market.read().await;
58 debug!(elapsed = start_time.elapsed().as_micros(), "market_guard market.read acquired");
59
60 for (pool, v) in state_update_event.directions().iter() {
61 let pool_paths: Vec<SwapPath> = match market_guard_read.get_pool_paths(&pool.get_pool_id()) {
62 Some(paths) => {
63 paths
71 .into_iter()
72 .enumerate()
73 .filter(|(idx, swap_path)| {
74 *idx < 100 || swap_path.score.unwrap_or_default() > 0.97
75 })
77 .map(|(_, swap_path)| swap_path)
78 .collect::<Vec<_>>()
79 }
80
81 None => {
82 let mut pool_direction: BTreeMap<PoolWrapper, Vec<SwapDirection>> = BTreeMap::new();
83 pool_direction.insert(pool.clone(), v.clone());
84 market_guard_read.build_swap_path_vec(&pool_direction).unwrap_or_default()
85 }
86 };
87
88 for pool_path in pool_paths {
89 swap_path_set.insert(pool_path);
90 }
91 }
92 drop(market_guard_read);
93 debug!(elapsed = start_time.elapsed().as_micros(), "market_guard market.read released");
94
95 let swap_path_vec: Vec<SwapPath> = swap_path_set.into_iter().collect();
96
97 if swap_path_vec.is_empty() {
98 debug!(
99 request=?state_update_event.stuffing_txs_hashes().first(),
100 elapsed=start_time.elapsed().as_micros(),
101 "No swap path built",
102
103 );
104 return Err(eyre!("NO_SWAP_PATHS"));
105 }
106
107 debug!("Affected pools: {} unique pools, generated {} swap paths", state_update_event.directions().len(), swap_path_vec.len());
109
110 if state_update_event.directions().len() <= 3 && swap_path_vec.len() > 50 {
112 debug!(
113 "Warning: {} swap paths generated from only {} pools - this may indicate duplicate path generation",
114 swap_path_vec.len(),
115 state_update_event.directions().len()
116 );
117 }
118
119 info!("Calculation started: swap_path_vec_len={} elapsed={}", swap_path_vec.len(), start_time.elapsed().as_micros());
120
121 let channel_len = swap_path_vec.len();
122 let (swap_path_tx, mut swap_line_rx) = tokio::sync::mpsc::channel(channel_len);
123
124 let block_env = BlockEnv {
125 number: U256::from(state_update_event.next_block_number),
126 timestamp: U256::from(state_update_event.next_block_timestamp),
127 basefee: state_update_event.next_base_fee,
128 blob_excess_gas_and_price: Some(BlobExcessGasAndPrice { excess_blob_gas: 0, blob_gasprice: 0 }),
129 ..Default::default()
130 };
131 let evm_env = EvmEnv::new(CfgEnv::new(), block_env);
132 let market_state_clone = db.clone();
133 let swap_path_vec_len = swap_path_vec.len();
134
135 let _tasks = tokio::task::spawn(async move {
136 thread_pool.install(|| {
137 swap_path_vec.into_par_iter().for_each_with((&swap_path_tx, &market_state_clone, evm_env), |(_req, _db, evm_env), item| {
138 let mut mut_item: SwapLine = SwapLine { path: item, ..Default::default() };
139
140 let calc_result = SwapCalculator::calculate(&mut mut_item, &market_state_clone, evm_env.clone());
141
142 match calc_result {
143 Ok(_) => {
144 debug!("Calc result received: {}", mut_item);
145
146 if let Ok(profit) = mut_item.profit() {
147 if profit.is_positive() && mut_item.abs_profit_eth() > U256::from(state_update_event.next_base_fee * 100_000) {
148 if let Err(error) = swap_path_tx.try_send(Ok(mut_item)) {
149 error!(%error, "swap_path_tx.try_send")
150 }
151 } else {
152 debug!("profit is not enough")
153 }
154 }
155 }
156 Err(e) => {
157 debug!("Swap error: {:?}", e);
158
159 if let Err(error) = swap_path_tx.try_send(Err(e)) {
160 error!(%error, "try_send to swap_path_tx")
161 }
162 }
163 }
164 });
165 });
166 debug!(elapsed = start_time.elapsed().as_micros(), "Calculation iteration finished");
167 });
168
169 debug!(elapsed = start_time.elapsed().as_micros(), "Calculation results receiver started");
170
171 let swap_request_tx_clone = swap_request_tx.clone();
172 let pool_health_monitor_tx_clone = pool_health_monitor_tx.clone();
173
174 let mut answers = 0;
175
176 let mut best_answers = BestTxSwapCompose::new_with_pct(U256::from(5000)); let mut failed_pools: HashSet<SwapError> = HashSet::new();
179
180 while let Some(swap_line_result) = swap_line_rx.recv().await {
181 match swap_line_result {
182 Ok(swap_line) => {
183 let min_profit_wei = U256::from(1_000_000_000_000_000u64); if swap_line.abs_profit_eth() < min_profit_wei {
186 continue;
187 }
188
189 let prepare_request = SwapComposeMessage::Prepare(SwapComposeData {
190 tx_compose: TxComposeData::<LDT> {
191 eoa: backrun_config.eoa(),
192 next_block_number: state_update_event.next_block_number,
193 next_block_timestamp: state_update_event.next_block_timestamp,
194 next_block_base_fee: state_update_event.next_base_fee,
195 gas: swap_line.gas_used.unwrap_or(300000),
196 stuffing_txs: state_update_event.stuffing_txs().clone(),
197 stuffing_txs_hashes: state_update_event.stuffing_txs_hashes.clone(),
198 ..TxComposeData::default()
199 },
200 swap: Swap::BackrunSwapLine(swap_line),
201 origin: Some(state_update_event.origin.clone()),
202 tips_pct: Some(state_update_event.tips_pct),
203 poststate: Some(db.clone()),
204 poststate_update: Some(state_update_event.state_update().clone()),
205 ..SwapComposeData::default()
206 });
207
208 if (!backrun_config.smart() || best_answers.check(&prepare_request))
209 && let Err(e) = swap_request_tx_clone.send(Message::new(prepare_request))
210 {
211 error!("swap_request_tx_clone.send {}", e)
212 }
213 }
214 Err(swap_error) => {
215 if failed_pools.insert(swap_error.clone())
216 && let Err(e) = pool_health_monitor_tx_clone.send(Message::new(HealthEvent::PoolSwapError(swap_error)))
217 {
218 error!("try_send to pool_health_monitor error : {:?}", e)
219 }
220 }
221 }
222
223 answers += 1;
224 }
225
226 let stuffing_tx_hash = state_update_event.stuffing_tx_hash();
227 let elapsed = start_time.elapsed().as_micros();
228 info!(
229 origin = %state_update_event.origin,
230 swap_path_vec_len,
231 answers,
232 elapsed,
233 stuffing_hash = %stuffing_tx_hash,
234 "Calculation finished"
235 );
236
237 let write_query = WriteQuery::new(Timestamp::from(start_time_utc), "calculations")
238 .add_field("calculations", swap_path_vec_len as u64)
239 .add_field("answers", answers as u64)
240 .add_field("elapsed", elapsed as u64)
241 .add_tag("origin", state_update_event.origin)
242 .add_tag("stuffing", stuffing_tx_hash.to_string());
243
244 if let Some(tx) = influxdb_write_channel_tx
245 && let Err(e) = tx.send(write_query)
246 {
247 error!("Failed to send block latency to influxdb: {:?}", e);
248 }
249
250 Ok(())
251}
252
253pub async fn state_change_arb_searcher_worker<
254 DB: DatabaseRef<Error = KabuDBError> + Database<Error = KabuDBError> + DatabaseCommit + Send + Sync + Clone + Default + 'static,
255 LDT: NodePrimitives + 'static,
256>(
257 backrun_config: BackrunConfig,
258 market: Arc<RwLock<Market>>,
259 search_request_rx: broadcast::Receiver<StateUpdateEvent<DB, LDT>>,
260 swap_request_tx: broadcast::Sender<MessageSwapCompose<DB, LDT>>,
261 pool_health_monitor_tx: broadcast::Sender<MessageHealthEvent>,
262 influxdb_write_channel_tx: Option<broadcast::Sender<WriteQuery>>,
263) -> Result<()> {
264 let mut search_request_receiver = search_request_rx.resubscribe();
265
266 let cpus = num_cpus::get();
267 let tasks = (cpus * 5) / 10;
268 info!("Starting state arb searcher cpus={cpus}, tasks={tasks}");
269 let thread_pool = Arc::new(ThreadPoolBuilder::new().num_threads(tasks).build()?);
270
271 let mut recent_pool_updates: HashSet<(u64, Vec<PoolWrapper>)> = HashSet::new();
273 let mut last_block_number = 0u64;
274
275 loop {
276 tokio::select! {
277 msg = search_request_receiver.recv() => {
278 let pool_update_msg : Result<StateUpdateEvent<DB, LDT>, RecvError> = msg;
279 if let Ok(msg) = pool_update_msg {
280 if msg.next_block_number != last_block_number {
282 recent_pool_updates.clear();
283 last_block_number = msg.next_block_number;
284 }
285
286 let mut affected_pools: Vec<PoolWrapper> = msg.directions().keys().cloned().collect();
288 affected_pools.sort_by_key(|p| p.get_pool_id());
289
290 let pool_update_key = (msg.next_block_number, affected_pools);
291
292 if recent_pool_updates.contains(&pool_update_key) {
294 debug!(
295 "Skipping duplicate state update for block {} with {} pools",
296 msg.next_block_number,
297 pool_update_key.1.len()
298 );
299 continue;
300 }
301
302 recent_pool_updates.insert(pool_update_key);
303
304 tokio::task::spawn(
305 state_change_arb_searcher_task(
306 thread_pool.clone(),
307 backrun_config.clone(),
308 msg,
309 market.clone(),
310 swap_request_tx.clone(),
311 pool_health_monitor_tx.clone(),
312 influxdb_write_channel_tx.clone(),
313 )
314 );
315 }
316 }
317 }
318 }
319}
320
321pub struct StateChangeArbSearcherComponent<DB: Clone + Send + Sync + 'static, LDT: NodePrimitives + 'static> {
322 backrun_config: BackrunConfig,
323
324 market: Option<Arc<RwLock<Market>>>,
325
326 state_update_rx: Option<broadcast::Sender<StateUpdateEvent<DB, LDT>>>,
327
328 swap_tx: Option<broadcast::Sender<MessageSwapCompose<DB, LDT>>>,
329
330 pool_health_monitor_tx: Option<broadcast::Sender<MessageHealthEvent>>,
331
332 influxdb_write_channel_tx: Option<broadcast::Sender<WriteQuery>>,
333}
334
335impl<
336 DB: DatabaseRef<Error = KabuDBError> + Database<Error = KabuDBError> + DatabaseCommit + Send + Sync + Clone + 'static,
337 LDT: NodePrimitives + 'static,
338> StateChangeArbSearcherComponent<DB, LDT>
339{
340 pub fn new(backrun_config: BackrunConfig) -> StateChangeArbSearcherComponent<DB, LDT> {
341 StateChangeArbSearcherComponent {
342 backrun_config,
343 market: None,
344 state_update_rx: None,
345 swap_tx: None,
346 pool_health_monitor_tx: None,
347 influxdb_write_channel_tx: None,
348 }
349 }
350
351 pub fn with_channels(
352 self,
353 state_update_rx: broadcast::Sender<StateUpdateEvent<DB, LDT>>,
354 swap_tx: broadcast::Sender<MessageSwapCompose<DB, LDT>>,
355 pool_health_monitor_tx: broadcast::Sender<MessageHealthEvent>,
356 influxdb_write_channel_tx: Option<broadcast::Sender<WriteQuery>>,
357 ) -> Self {
358 Self {
359 state_update_rx: Some(state_update_rx),
360 swap_tx: Some(swap_tx),
361 pool_health_monitor_tx: Some(pool_health_monitor_tx),
362 influxdb_write_channel_tx,
363 ..self
364 }
365 }
366
367 pub fn with_market(self, market: Arc<RwLock<Market>>) -> Self {
368 Self { market: Some(market), ..self }
369 }
370}
371
372impl<DB, LDT> Component for StateChangeArbSearcherComponent<DB, LDT>
373where
374 DB: Database<Error = KabuDBError> + DatabaseRef<Error = KabuDBError> + DatabaseCommit + Send + Sync + Clone + Default + 'static,
375 LDT: NodePrimitives + 'static,
376{
377 fn spawn(self, executor: TaskExecutor) -> Result<()> {
378 let name = self.name();
379
380 let state_update_rx = self.state_update_rx.ok_or_else(|| eyre!("state_update_rx not set"))?.subscribe();
381 let swap_tx = self.swap_tx.ok_or_else(|| eyre!("swap_tx not set"))?;
382 let market = self.market.ok_or_else(|| eyre!("market not set"))?;
383 let pool_health_monitor_tx = self.pool_health_monitor_tx.ok_or_else(|| eyre!("pool_health_monitor_tx not set"))?;
384 let influxdb_write_channel = self.influxdb_write_channel_tx;
385 let backrun_config = self.backrun_config.clone();
386
387 executor.spawn_critical(name, async move {
388 if let Err(e) = state_change_arb_searcher_worker(
389 backrun_config,
390 market,
391 state_update_rx,
392 swap_tx,
393 pool_health_monitor_tx,
394 influxdb_write_channel,
395 )
396 .await
397 {
398 error!("state_change_arb_searcher_worker failed: {}", e);
399 }
400 });
401
402 Ok(())
403 }
404
405 fn name(&self) -> &'static str {
406 "StateChangeArbSearcherComponent"
407 }
408}