[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-16-bandling-razreshenie-konfliktov-upakovka":3},{"article":4,"author":59},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":16,"meta_description":17,"focus_keyword":18,"og_image":19,"canonical_url":19,"robots_meta":20,"created_at":15,"updated_at":15,"tags":21,"category_name":39,"related_articles":40},"d0000000-0000-0000-0000-000000000216","a0000000-0000-0000-0000-000000000016","Deep EVM #16: Бандлинг и разрешение конфликтов — упаковка прибыльных транзакций","deep-evm-16-bandling-razreshenie-konfliktov-upakovka","Финальная стадия MEV-пайплайна: упаковка множества арбитражных транзакций в бандл, разрешение конфликтов доступа к стейту и отправка через Flashbots.","## От одной транзакции к бандлу\n\nВ предыдущих статьях мы нашли арбитражные маршруты и оптимизировали каждый по отдельности. Но серчер может обнаружить десятки прибыльных возможностей в одном блоке. Вопрос: можно ли исполнить их все?\n\nОтвет: не всегда. Две арбитражные транзакции могут **конфликтовать** — если они трогают одни и те же пулы, первая транзакция меняет резервы, и вторая становится убыточной. Задача бандлинга — выбрать максимально прибыльное подмножество совместимых транзакций.\n\n## Формализация конфликтов\n\nДве транзакции конфликтуют, если множества пулов, к которым они обращаются, пересекаются:\n\n```rust\nuse std::collections::HashSet;\n\n#[derive(Clone, Debug)]\npub struct MevOpportunity {\n    pub id: usize,\n    pub route: ArbitrageRoute,\n    pub input: U256,\n    pub gross_profit: U256,\n    pub gas_used: u64,\n    pub accessed_pools: HashSet\u003CAddress>,\n}\n\nimpl MevOpportunity {\n    pub fn conflicts_with(&self, other: &MevOpportunity) -> bool {\n        self.accessed_pools\n            .intersection(&other.accessed_pools)\n            .next()\n            .is_some()\n    }\n}\n```\n\n## Граф конфликтов\n\nПостроим граф, где узлы — MEV-возможности, рёбра — конфликты. Задача выбора максимально прибыльного подмножества без конфликтов — это задача Maximum Weight Independent Set (MWIS) на графе конфликтов.\n\n```rust\npub struct ConflictGraph {\n    pub opportunities: Vec\u003CMevOpportunity>,\n    pub conflicts: Vec\u003CVec\u003Cbool>>,  \u002F\u002F матрица смежности\n}\n\nimpl ConflictGraph {\n    pub fn build(opportunities: Vec\u003CMevOpportunity>) -> Self {\n        let n = opportunities.len();\n        let mut conflicts = vec![vec![false; n]; n];\n        \n        for i in 0..n {\n            for j in (i + 1)..n {\n                if opportunities[i].conflicts_with(&opportunities[j]) {\n                    conflicts[i][j] = true;\n                    conflicts[j][i] = true;\n                }\n            }\n        }\n        \n        Self { opportunities, conflicts }\n    }\n}\n```\n\nMWIS — NP-трудная задача в общем случае, но на практике граф конфликтов MEV-возможностей имеет специфическую структуру: большинство возможностей не конфликтуют (разные пулы), и граф разреженный.\n\n## Жадный алгоритм\n\nДля десятков возможностей жадный алгоритм даёт хороший результат:\n\n```rust\nimpl ConflictGraph {\n    pub fn greedy_selection(&self) -> Vec\u003Cusize> {\n        let n = self.opportunities.len();\n        \n        \u002F\u002F Сортируем по прибыли (убывание)\n        let mut indices: Vec\u003Cusize> = (0..n).collect();\n        indices.sort_by(|&a, &b| {\n            self.opportunities[b].gross_profit\n                .cmp(&self.opportunities[a].gross_profit)\n        });\n        \n        let mut selected = Vec::new();\n        let mut excluded = vec![false; n];\n        \n        for &i in &indices {\n            if excluded[i] {\n                continue;\n            }\n            \n            selected.push(i);\n            \n            \u002F\u002F Исключаем все конфликтующие\n            for j in 0..n {\n                if self.conflicts[i][j] {\n                    excluded[j] = true;\n                }\n            }\n        }\n        \n        selected\n    }\n}\n```\n\n### Время работы\n\nЖадный алгоритм: O(n^2) для построения графа конфликтов + O(n log n) для сортировки + O(n^2) для отбора. Для n=100 это микросекунды.\n\n## Точный алгоритм для малых графов\n\nКогда возможностей мало (n \u003C= 20), можно решить точно через bitmask DP:\n\n```rust\npub fn exact_mwis(graph: &ConflictGraph) -> Vec\u003Cusize> {\n    let n = graph.opportunities.len();\n    assert!(n \u003C= 20, \"Too many opportunities for exact solution\");\n    \n    let mut best_profit = U256::ZERO;\n    let mut best_mask = 0u32;\n    \n    \u002F\u002F Предвычисляем маски конфликтов\n    let mut conflict_mask = vec![0u32; n];\n    for i in 0..n {\n        for j in 0..n {\n            if graph.conflicts[i][j] {\n                conflict_mask[i] |= 1 \u003C\u003C j;\n            }\n        }\n    }\n    \n    \u002F\u002F Перебираем все подмножества\n    for mask in 0..(1u32 \u003C\u003C n) {\n        \u002F\u002F Проверяем что нет конфликтов внутри подмножества\n        let mut valid = true;\n        for i in 0..n {\n            if mask & (1 \u003C\u003C i) != 0 {\n                if mask & conflict_mask[i] & !(1 \u003C\u003C i) != 0 {\n                    valid = false;\n                    break;\n                }\n            }\n        }\n        \n        if !valid {\n            continue;\n        }\n        \n        \u002F\u002F Считаем суммарную прибыль\n        let mut profit = U256::ZERO;\n        for i in 0..n {\n            if mask & (1 \u003C\u003C i) != 0 {\n                profit += graph.opportunities[i].gross_profit;\n            }\n        }\n        \n        if profit > best_profit {\n            best_profit = profit;\n            best_mask = mask;\n        }\n    }\n    \n    \u002F\u002F Декодируем маску в индексы\n    (0..n).filter(|&i| best_mask & (1 \u003C\u003C i) != 0).collect()\n}\n```\n\n## Пересимуляция после разрешения конфликтов\n\nВажный нюанс: даже не конфликтующие транзакции могут влиять друг на друга через непрямые эффекты (например, изменение base fee). Поэтому после выбора подмножества — пересимулируем весь бандл последовательно:\n\n```rust\npub fn validate_bundle(\n    forker: &mut StateForker,\n    opportunities: &[&MevOpportunity],\n) -> Vec\u003C(usize, U256)> {\n    let mut results = Vec::new();\n    \n    \u002F\u002F Симулируем транзакции последовательно на одном стейте\n    for opp in opportunities {\n        let snapshot = forker.snapshot();\n        let calldata = encode_arbitrage_calldata(&opp.route, opp.input);\n        let result = forker.simulate_call(\n            SEARCHER_ADDRESS,\n            CONTRACT_ADDRESS,\n            calldata,\n            U256::ZERO,\n        );\n        \n        if result.success {\n            let profit = decode_profit(&result.output);\n            if profit > U256::ZERO {\n                results.push((opp.id, profit));\n                \u002F\u002F НЕ откатываем — следующая транзакция видит изменённый стейт\n                continue;\n            }\n        }\n        \n        \u002F\u002F Откатываем неуспешную транзакцию\n        forker.rollback(snapshot);\n    }\n    \n    results\n}\n```\n\n## Сборка и отправка бандла\n\nПосле валидации собираем Flashbots-бандл:\n\n```rust\nuse ethers_flashbots::{BundleRequest, FlashbotsMiddleware};\n\npub async fn submit_flashbots_bundle(\n    flashbots: &FlashbotsMiddleware,\n    opportunities: &[&MevOpportunity],\n    target_block: u64,\n) -> Result\u003C(), Box\u003Cdyn std::error::Error>> {\n    let mut bundle = BundleRequest::new();\n    \n    for opp in opportunities {\n        let tx = build_transaction(&opp.route, opp.input).await?;\n        bundle = bundle.push_transaction(tx);\n    }\n    \n    bundle = bundle\n        .set_block(target_block)\n        .set_simulation_block(target_block - 1);\n    \n    let pending = flashbots\n        .inner()\n        .send_bundle(&bundle)\n        .await?;\n    \n    \u002F\u002F Ждём результат\n    match pending.await? {\n        Some(receipt) => {\n            println!(\"Bundle included in block {}\", receipt.block_number);\n        }\n        None => {\n            println!(\"Bundle not included — competing searcher won\");\n        }\n    }\n    \n    Ok(())\n}\n```\n\n## Стратегия приоритетной комиссии\n\nСколько отдавать билдеру? Слишком мало — бандл не включат. Слишком много — нет прибыли.\n\n```rust\npub fn calculate_priority_fee(\n    total_profit: U256,\n    gas_used: u64,\n    base_fee: U256,\n    competition_level: f64,  \u002F\u002F 0.0-1.0\n) -> U256 {\n    let gas_cost = U256::from(gas_used) * base_fee;\n    let net_after_gas = total_profit.saturating_sub(gas_cost);\n    \n    \u002F\u002F Отдаём 85-99% в зависимости от конкуренции\n    let share = 0.85 + competition_level * 0.14;  \u002F\u002F 85% - 99%\n    let tip = (net_after_gas.as_u128() as f64 * share) as u128;\n    \n    U256::from(tip)\n}\n```\n\nВ реальности серчеры мониторят, какой процент их бандлов включается, и динамически регулируют долю.\n\n## Полный пайплайн серчера\n\n```rust\npub async fn full_pipeline(\n    graph: &ArbitrageGraph,\n    forker: &mut StateForker,\n    flashbots: &FlashbotsMiddleware,\n    block: &Block,\n) {\n    \u002F\u002F 1. Поиск маршрутов (~50 мс)\n    let routes = graph.find_cycles(WETH, 3);\n    \n    \u002F\u002F 2. Симуляция и оптимизация (~200 мс)\n    let mut opportunities = Vec::new();\n    for route in routes.iter().take(50) {\n        let snapshot = forker.snapshot();\n        let (input, profit) = find_optimal_input(\n            forker, route, MIN_INPUT, MAX_INPUT, 30,\n        );\n        forker.rollback(snapshot);\n        \n        if profit > MIN_PROFIT_THRESHOLD {\n            opportunities.push(MevOpportunity {\n                id: opportunities.len(),\n                route: route.clone(),\n                input,\n                gross_profit: profit,\n                gas_used: 300_000,\n                accessed_pools: route.pool_set(),\n            });\n        }\n    }\n    \n    \u002F\u002F 3. Разрешение конфликтов (~1 мс)\n    let conflict_graph = ConflictGraph::build(opportunities);\n    let selected = if conflict_graph.opportunities.len() \u003C= 20 {\n        exact_mwis(&conflict_graph)\n    } else {\n        conflict_graph.greedy_selection()\n    };\n    \n    \u002F\u002F 4. Валидация бандла (~100 мс)\n    let selected_opps: Vec\u003C_> = selected.iter()\n        .map(|&i| &conflict_graph.opportunities[i])\n        .collect();\n    let validated = validate_bundle(forker, &selected_opps);\n    \n    \u002F\u002F 5. Отправка (~10 мс)\n    if !validated.is_empty() {\n        let total_profit: U256 = validated.iter()\n            .map(|(_, p)| *p)\n            .sum();\n        submit_flashbots_bundle(\n            flashbots,\n            &selected_opps,\n            block.number + 1,\n        ).await;\n    }\n}\n```\n\n## Мониторинг и метрики\n\nProduction-серчер отслеживает:\n\n- **Inclusion rate** — доля включённых бандлов\n- **Revenue per block** — средняя прибыль за блок\n- **Latency P99** — 99-й перцентиль времени пайплайна\n- **Missed opportunities** — возможности, упущенные из-за таймаута\n\n```rust\nuse prometheus::{IntCounter, Histogram, register_int_counter, register_histogram};\n\nlazy_static! {\n    static ref BUNDLES_SUBMITTED: IntCounter = register_int_counter!(\n        \"bundles_submitted_total\", \"Total bundles submitted\"\n    ).unwrap();\n    static ref BUNDLES_INCLUDED: IntCounter = register_int_counter!(\n        \"bundles_included_total\", \"Total bundles included in blocks\"\n    ).unwrap();\n    static ref PIPELINE_LATENCY: Histogram = register_histogram!(\n        \"pipeline_latency_seconds\", \"Pipeline execution time\"\n    ).unwrap();\n}\n```\n\n## Итоги\n\nБандлинг — финальная стадия MEV-пайплайна. Граф конфликтов формализует зависимости между возможностями. Жадный алгоритм даёт хороший результат за O(n^2), точный bitmask DP решает задачу для n \u003C= 20. Пересимуляция бандла на едином стейте подтверждает реальную прибыль. Flashbots API доставляет бандл билдерам. Цикл из 16 статей Deep EVM завершён: от опкодов до production MEV-серчера.","\u003Ch2 id=\"\">От одной транзакции к бандлу\u003C\u002Fh2>\n\u003Cp>В предыдущих статьях мы нашли арбитражные маршруты и оптимизировали каждый по отдельности. Но серчер может обнаружить десятки прибыльных возможностей в одном блоке. Вопрос: можно ли исполнить их все?\u003C\u002Fp>\n\u003Cp>Ответ: не всегда. Две арбитражные транзакции могут \u003Cstrong>конфликтовать\u003C\u002Fstrong> — если они трогают одни и те же пулы, первая транзакция меняет резервы, и вторая становится убыточной. Задача бандлинга — выбрать максимально прибыльное подмножество совместимых транзакций.\u003C\u002Fp>\n\u003Ch2 id=\"\">Формализация конфликтов\u003C\u002Fh2>\n\u003Cp>Две транзакции конфликтуют, если множества пулов, к которым они обращаются, пересекаются:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use std::collections::HashSet;\n\n#[derive(Clone, Debug)]\npub struct MevOpportunity {\n    pub id: usize,\n    pub route: ArbitrageRoute,\n    pub input: U256,\n    pub gross_profit: U256,\n    pub gas_used: u64,\n    pub accessed_pools: HashSet&lt;Address&gt;,\n}\n\nimpl MevOpportunity {\n    pub fn conflicts_with(&amp;self, other: &amp;MevOpportunity) -&gt; bool {\n        self.accessed_pools\n            .intersection(&amp;other.accessed_pools)\n            .next()\n            .is_some()\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Граф конфликтов\u003C\u002Fh2>\n\u003Cp>Построим граф, где узлы — MEV-возможности, рёбра — конфликты. Задача выбора максимально прибыльного подмножества без конфликтов — это задача Maximum Weight Independent Set (MWIS) на графе конфликтов.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub struct ConflictGraph {\n    pub opportunities: Vec&lt;MevOpportunity&gt;,\n    pub conflicts: Vec&lt;Vec&lt;bool&gt;&gt;,  \u002F\u002F матрица смежности\n}\n\nimpl ConflictGraph {\n    pub fn build(opportunities: Vec&lt;MevOpportunity&gt;) -&gt; Self {\n        let n = opportunities.len();\n        let mut conflicts = vec![vec![false; n]; n];\n        \n        for i in 0..n {\n            for j in (i + 1)..n {\n                if opportunities[i].conflicts_with(&amp;opportunities[j]) {\n                    conflicts[i][j] = true;\n                    conflicts[j][i] = true;\n                }\n            }\n        }\n        \n        Self { opportunities, conflicts }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>MWIS — NP-трудная задача в общем случае, но на практике граф конфликтов MEV-возможностей имеет специфическую структуру: большинство возможностей не конфликтуют (разные пулы), и граф разреженный.\u003C\u002Fp>\n\u003Ch2 id=\"\">Жадный алгоритм\u003C\u002Fh2>\n\u003Cp>Для десятков возможностей жадный алгоритм даёт хороший результат:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">impl ConflictGraph {\n    pub fn greedy_selection(&amp;self) -&gt; Vec&lt;usize&gt; {\n        let n = self.opportunities.len();\n        \n        \u002F\u002F Сортируем по прибыли (убывание)\n        let mut indices: Vec&lt;usize&gt; = (0..n).collect();\n        indices.sort_by(|&amp;a, &amp;b| {\n            self.opportunities[b].gross_profit\n                .cmp(&amp;self.opportunities[a].gross_profit)\n        });\n        \n        let mut selected = Vec::new();\n        let mut excluded = vec![false; n];\n        \n        for &amp;i in &amp;indices {\n            if excluded[i] {\n                continue;\n            }\n            \n            selected.push(i);\n            \n            \u002F\u002F Исключаем все конфликтующие\n            for j in 0..n {\n                if self.conflicts[i][j] {\n                    excluded[j] = true;\n                }\n            }\n        }\n        \n        selected\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Время работы\u003C\u002Fh3>\n\u003Cp>Жадный алгоритм: O(n^2) для построения графа конфликтов + O(n log n) для сортировки + O(n^2) для отбора. Для n=100 это микросекунды.\u003C\u002Fp>\n\u003Ch2 id=\"\">Точный алгоритм для малых графов\u003C\u002Fh2>\n\u003Cp>Когда возможностей мало (n &lt;= 20), можно решить точно через bitmask DP:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub fn exact_mwis(graph: &amp;ConflictGraph) -&gt; Vec&lt;usize&gt; {\n    let n = graph.opportunities.len();\n    assert!(n &lt;= 20, \"Too many opportunities for exact solution\");\n    \n    let mut best_profit = U256::ZERO;\n    let mut best_mask = 0u32;\n    \n    \u002F\u002F Предвычисляем маски конфликтов\n    let mut conflict_mask = vec![0u32; n];\n    for i in 0..n {\n        for j in 0..n {\n            if graph.conflicts[i][j] {\n                conflict_mask[i] |= 1 &lt;&lt; j;\n            }\n        }\n    }\n    \n    \u002F\u002F Перебираем все подмножества\n    for mask in 0..(1u32 &lt;&lt; n) {\n        \u002F\u002F Проверяем что нет конфликтов внутри подмножества\n        let mut valid = true;\n        for i in 0..n {\n            if mask &amp; (1 &lt;&lt; i) != 0 {\n                if mask &amp; conflict_mask[i] &amp; !(1 &lt;&lt; i) != 0 {\n                    valid = false;\n                    break;\n                }\n            }\n        }\n        \n        if !valid {\n            continue;\n        }\n        \n        \u002F\u002F Считаем суммарную прибыль\n        let mut profit = U256::ZERO;\n        for i in 0..n {\n            if mask &amp; (1 &lt;&lt; i) != 0 {\n                profit += graph.opportunities[i].gross_profit;\n            }\n        }\n        \n        if profit &gt; best_profit {\n            best_profit = profit;\n            best_mask = mask;\n        }\n    }\n    \n    \u002F\u002F Декодируем маску в индексы\n    (0..n).filter(|&amp;i| best_mask &amp; (1 &lt;&lt; i) != 0).collect()\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Пересимуляция после разрешения конфликтов\u003C\u002Fh2>\n\u003Cp>Важный нюанс: даже не конфликтующие транзакции могут влиять друг на друга через непрямые эффекты (например, изменение base fee). Поэтому после выбора подмножества — пересимулируем весь бандл последовательно:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub fn validate_bundle(\n    forker: &amp;mut StateForker,\n    opportunities: &amp;[&amp;MevOpportunity],\n) -&gt; Vec&lt;(usize, U256)&gt; {\n    let mut results = Vec::new();\n    \n    \u002F\u002F Симулируем транзакции последовательно на одном стейте\n    for opp in opportunities {\n        let snapshot = forker.snapshot();\n        let calldata = encode_arbitrage_calldata(&amp;opp.route, opp.input);\n        let result = forker.simulate_call(\n            SEARCHER_ADDRESS,\n            CONTRACT_ADDRESS,\n            calldata,\n            U256::ZERO,\n        );\n        \n        if result.success {\n            let profit = decode_profit(&amp;result.output);\n            if profit &gt; U256::ZERO {\n                results.push((opp.id, profit));\n                \u002F\u002F НЕ откатываем — следующая транзакция видит изменённый стейт\n                continue;\n            }\n        }\n        \n        \u002F\u002F Откатываем неуспешную транзакцию\n        forker.rollback(snapshot);\n    }\n    \n    results\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Сборка и отправка бандла\u003C\u002Fh2>\n\u003Cp>После валидации собираем Flashbots-бандл:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">use ethers_flashbots::{BundleRequest, FlashbotsMiddleware};\n\npub async fn submit_flashbots_bundle(\n    flashbots: &amp;FlashbotsMiddleware,\n    opportunities: &amp;[&amp;MevOpportunity],\n    target_block: u64,\n) -&gt; Result&lt;(), Box&lt;dyn std::error::Error&gt;&gt; {\n    let mut bundle = BundleRequest::new();\n    \n    for opp in opportunities {\n        let tx = build_transaction(&amp;opp.route, opp.input).await?;\n        bundle = bundle.push_transaction(tx);\n    }\n    \n    bundle = bundle\n        .set_block(target_block)\n        .set_simulation_block(target_block - 1);\n    \n    let pending = flashbots\n        .inner()\n        .send_bundle(&amp;bundle)\n        .await?;\n    \n    \u002F\u002F Ждём результат\n    match pending.await? {\n        Some(receipt) =&gt; {\n            println!(\"Bundle included in block {}\", receipt.block_number);\n        }\n        None =&gt; {\n            println!(\"Bundle not included — competing searcher won\");\n        }\n    }\n    \n    Ok(())\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Стратегия приоритетной комиссии\u003C\u002Fh2>\n\u003Cp>Сколько отдавать билдеру? Слишком мало — бандл не включат. Слишком много — нет прибыли.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub fn calculate_priority_fee(\n    total_profit: U256,\n    gas_used: u64,\n    base_fee: U256,\n    competition_level: f64,  \u002F\u002F 0.0-1.0\n) -&gt; U256 {\n    let gas_cost = U256::from(gas_used) * base_fee;\n    let net_after_gas = total_profit.saturating_sub(gas_cost);\n    \n    \u002F\u002F Отдаём 85-99% в зависимости от конкуренции\n    let share = 0.85 + competition_level * 0.14;  \u002F\u002F 85% - 99%\n    let tip = (net_after_gas.as_u128() as f64 * share) as u128;\n    \n    U256::from(tip)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>В реальности серчеры мониторят, какой процент их бандлов включается, и динамически регулируют долю.\u003C\u002Fp>\n\u003Ch2 id=\"\">Полный пайплайн серчера\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-rust\">pub async fn full_pipeline(\n    graph: &amp;ArbitrageGraph,\n    forker: &amp;mut StateForker,\n    flashbots: &amp;FlashbotsMiddleware,\n    block: &amp;Block,\n) {\n    \u002F\u002F 1. Поиск маршрутов (~50 мс)\n    let routes = graph.find_cycles(WETH, 3);\n    \n    \u002F\u002F 2. Симуляция и оптимизация (~200 мс)\n    let mut opportunities = Vec::new();\n    for route in routes.iter().take(50) {\n        let snapshot = forker.snapshot();\n        let (input, profit) = find_optimal_input(\n            forker, route, MIN_INPUT, MAX_INPUT, 30,\n        );\n        forker.rollback(snapshot);\n        \n        if profit &gt; MIN_PROFIT_THRESHOLD {\n            opportunities.push(MevOpportunity {\n                id: opportunities.len(),\n                route: route.clone(),\n                input,\n                gross_profit: profit,\n                gas_used: 300_000,\n                accessed_pools: route.pool_set(),\n            });\n        }\n    }\n    \n    \u002F\u002F 3. Разрешение конфликтов (~1 мс)\n    let conflict_graph = ConflictGraph::build(opportunities);\n    let selected = if conflict_graph.opportunities.len() &lt;= 20 {\n        exact_mwis(&amp;conflict_graph)\n    } else {\n        conflict_graph.greedy_selection()\n    };\n    \n    \u002F\u002F 4. Валидация бандла (~100 мс)\n    let selected_opps: Vec&lt;_&gt; = selected.iter()\n        .map(|&amp;i| &amp;conflict_graph.opportunities[i])\n        .collect();\n    let validated = validate_bundle(forker, &amp;selected_opps);\n    \n    \u002F\u002F 5. Отправка (~10 мс)\n    if !validated.is_empty() {\n        let total_profit: U256 = validated.iter()\n            .map(|(_, p)| *p)\n            .sum();\n        submit_flashbots_bundle(\n            flashbots,\n            &amp;selected_opps,\n            block.number + 1,\n        ).await;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Мониторинг и метрики\u003C\u002Fh2>\n\u003Cp>Production-серчер отслеживает:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Inclusion rate\u003C\u002Fstrong> — доля включённых бандлов\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Revenue per block\u003C\u002Fstrong> — средняя прибыль за блок\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Latency P99\u003C\u002Fstrong> — 99-й перцентиль времени пайплайна\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Missed opportunities\u003C\u002Fstrong> — возможности, упущенные из-за таймаута\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cpre>\u003Ccode class=\"language-rust\">use prometheus::{IntCounter, Histogram, register_int_counter, register_histogram};\n\nlazy_static! {\n    static ref BUNDLES_SUBMITTED: IntCounter = register_int_counter!(\n        \"bundles_submitted_total\", \"Total bundles submitted\"\n    ).unwrap();\n    static ref BUNDLES_INCLUDED: IntCounter = register_int_counter!(\n        \"bundles_included_total\", \"Total bundles included in blocks\"\n    ).unwrap();\n    static ref PIPELINE_LATENCY: Histogram = register_histogram!(\n        \"pipeline_latency_seconds\", \"Pipeline execution time\"\n    ).unwrap();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">Итоги\u003C\u002Fh2>\n\u003Cp>Бандлинг — финальная стадия MEV-пайплайна. Граф конфликтов формализует зависимости между возможностями. Жадный алгоритм даёт хороший результат за O(n^2), точный bitmask DP решает задачу для n &lt;= 20. Пересимуляция бандла на едином стейте подтверждает реальную прибыль. Flashbots API доставляет бандл билдерам. Цикл из 16 статей Deep EVM завершён: от опкодов до production MEV-серчера.\u003C\u002Fp>\n","ru","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:23.685277Z","Deep EVM #16: Бандлинг и разрешение конфликтов — упаковка транзакций","Финальная стадия MEV: бандлинг транзакций, граф конфликтов, жадный алгоритм и bitmask DP, пересимуляция и отправка через Flashbots.","mev бандлинг конфликты flashbots",null,"index, follow",[22,27,31,35],{"id":23,"name":24,"slug":25,"created_at":26},"c0000000-0000-0000-0000-000000000019","MEV","mev","2026-03-28T10:44:21.513630Z",{"id":28,"name":29,"slug":30,"created_at":26},"c0000000-0000-0000-0000-000000000001","Rust","rust",{"id":32,"name":33,"slug":34,"created_at":26},"c0000000-0000-0000-0000-000000000013","Security","security",{"id":36,"name":37,"slug":38,"created_at":26},"c0000000-0000-0000-0000-000000000009","Web3","web3","Инженерия",[41,47,53],{"id":42,"title":43,"slug":44,"excerpt":45,"locale":12,"category_name":39,"published_at":46},"d0200000-0000-0000-0000-000000000013","Почему Бали становится хабом импакт-технологий Юго-Восточной Азии в 2026 году","pochemu-bali-stanovitsya-khabom-impakt-tekhnologiy-2026","Бали занимает 16-е место среди стартап-экосистем Юго-Восточной Азии. Растущая концентрация Web3-разработчиков, ИИ-стартапов в области устойчивого развития и компаний в сфере эко-тревел-технологий формирует нишу столицы импакт-технологий региона.","2026-03-28T10:44:37.953039Z",{"id":48,"title":49,"slug":50,"excerpt":51,"locale":12,"category_name":39,"published_at":52},"d0200000-0000-0000-0000-000000000012","Защита данных в ASEAN: чек-лист разработчика для мультистранового комплаенса","zashchita-dannykh-asean-chek-list-razrabotchika-komplaens","Семь стран ASEAN имеют собственные законы о защите данных с разными моделями согласия, требованиями к локализации и штрафами. Практический чек-лист для разработчиков мультистрановых приложений.","2026-03-28T10:44:37.944001Z",{"id":54,"title":55,"slug":56,"excerpt":57,"locale":12,"category_name":39,"published_at":58},"d0200000-0000-0000-0000-000000000011","Цифровая трансформация Индонезии на $29 миллиардов: возможности для софтверных компаний","tsifrovaya-transformatsiya-indonezii-29-milliardov-vozmozhnosti-dlya-kompaniy","Рынок IT-услуг Индонезии вырастет с $24,37 млрд в 2025 году до $29,03 млрд в 2026 году. Облачная инфраструктура, искусственный интеллект, электронная коммерция и дата-центры обеспечивают самый быстрый рост в Юго-Восточной Азии.","2026-03-28T10:44:37.917095Z",{"id":13,"name":60,"slug":61,"bio":62,"photo_url":19,"linkedin":19,"role":63,"created_at":64,"updated_at":64},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]