kabu_strategy_backrun/
state_change_arb_searcher.rs

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                // let pool_paths = pool_paths
64                //     .into_iter()
65                //     .enumerate()
66                //     .filter(|(idx, path)| *idx < 100 || path.score.unwrap_or_default() > 0.9)
67                //     .map(|(idx, path)| path)
68                //     .collect::<Vec<_>>();
69                // pool_paths
70                paths
71                    .into_iter()
72                    .enumerate()
73                    .filter(|(idx, swap_path)| {
74                        *idx < 100 || swap_path.score.unwrap_or_default() > 0.97
75                        //&& !swap_path.pools.iter().any(|pool| market_guard_read.is_pool_disabled(&pool.get_pool_id()))
76                    })
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    // Log details about the swap paths and affected pools
108    debug!("Affected pools: {} unique pools, generated {} swap paths", state_update_event.directions().len(), swap_path_vec.len());
109
110    // If we have very few pools but many paths, log a warning
111    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)); // Only send swaps that are at least 50% as good as the best
177
178    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                // Add minimum profit filter: skip if profit is less than 0.001 ETH
184                let min_profit_wei = U256::from(1_000_000_000_000_000u64); // 0.001 ETH
185                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    // Track recent state updates to prevent duplicate calculations for the same pools
272    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                    // Clear recent updates on new block
281                    if msg.next_block_number != last_block_number {
282                        recent_pool_updates.clear();
283                        last_block_number = msg.next_block_number;
284                    }
285
286                    // Create a sorted list of affected pools for consistent hashing
287                    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                    // Skip if we've already processed these exact pools in this block
293                    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}