Deep EVM #16: 번들링과 충돌 해결 — 수익성 있는 트랜잭션 패킹
Engineering Team
번들링이 중요한 이유
단일 차익거래 트랜잭션은 간단합니다: 시뮬레이션하고, 제출하고, 이익을 얻습니다. 그러나 프로덕션 MEV 봇은 블록당 수십 개의 수익성 있는 사이클을 찾습니다. 각 사이클은 하나의 트랜잭션을 생산합니다. 개별적으로 제출하면 낭비적이고 위험합니다:
- 가스 오버헤드: 각 트랜잭션은 21,000 기본 가스 비용을 지불합니다. N개의 스왑을 하나의 트랜잭션으로 번들링하면 기본 비용을 한 번만 지불합니다.
- 원자성: 트랜잭션 A와 B가 같은 풀을 건드리면, A의 실행이 B가 의존하는 상태를 변경합니다. 별도로 제출하면 B가 되돌릴 수 있습니다. 함께 번들링하면 올바르게 순서를 정할 수 있습니다.
- 빌더 선호: 블록 빌더는 더 많은 총 MEV를 지불하는 더 큰 번들을 선호합니다 — 번들은 다른 서처의 번들과 포함을 위해 경쟁합니다.
충돌 감지: 비트마스크 접근법
두 차익거래 사이클이 풀을 공유하면 충돌합니다. 사이클 A가 풀 X를 통해 스왑하면 풀 X의 준비금이 변경됩니다. 풀 X도 사용하는 사이클 B는 이전 준비금에 대해 시뮬레이션되었으므로 다른(아마도 더 나쁜) 결과를 생산합니다.
비트마스크를 사용하여 충돌을 감지합니다. 각 풀에 인덱스(0 ~ N-1)를 할당합니다. 각 사이클은 어떤 풀을 건드리는지를 나타내는 비트마스크를 받습니다:
use bitvec::prelude::*;
#[derive(Clone)]
pub struct CycleWithMask {
pub cycle: ProfitableCycle,
pub pool_mask: BitVec,
}
pub fn build_pool_masks(
cycles: &[ProfitableCycle],
pool_index: &HashMap<Address, usize>,
total_pools: usize,
) -> Vec<CycleWithMask> {
cycles
.iter()
.map(|c| {
let mut mask = bitvec![0; total_pools];
for pool in &c.cycle.pools {
if let Some(&idx) = pool_index.get(&pool.address) {
mask.set(idx, true);
}
}
CycleWithMask {
cycle: c.clone(),
pool_mask: mask,
}
})
.collect()
}
두 사이클의 비트마스크에 공통 비트가 있으면 충돌합니다:
fn conflicts(a: &BitVec, b: &BitVec) -> bool {
a.iter()
.zip(b.iter())
.any(|(x, y)| *x && *y)
}
비교당 O(N/64)입니다 — 각 64비트 워드가 단일 AND 명령으로 검사됩니다. 80,000개 풀의 경우 비트마스크는 10KB입니다. 두 마스크를 비교하는 데 약 150 AND 연산이 걸립니다. 풀 주소 목록을 순회하는 것보다 몇 배나 빠릅니다.
그리디 번들 패킹
수익성 있는 사이클 집합이 주어지면, 최대 이익 충돌 없는 부분 집합을 찾습니다. 이것은 충돌 그래프에서의 가중 최대 독립 집합 문제입니다 — 일반적으로 NP-난해합니다. 그리디 근사를 사용합니다:
pub fn pack_bundle(
candidates: &mut Vec<CycleWithMask>,
) -> Vec<CycleWithMask> {
// 이익 내림차순 정렬
candidates.sort_by(|a, b| {
b.cycle.expected_profit.cmp(&a.cycle.expected_profit)
});
let total_pools = candidates
.first()
.map(|c| c.pool_mask.len())
.unwrap_or(0);
let mut bundle: Vec<CycleWithMask> = Vec::new();
let mut used_pools = bitvec![0; total_pools];
for candidate in candidates.iter() {
// 이미 선택된 사이클과 충돌하는지 확인
if conflicts(&candidate.pool_mask, &used_pools) {
continue;
}
// 충돌 없음 — 번들에 추가
used_pools |= &candidate.pool_mask;
bundle.push(candidate.clone());
}
bundle
}
그리디 접근법은 가장 수익성 높은 사이클을 먼저 선택하고, 해당 풀을 사용됨으로 표시한 다음, 다음으로 수익성 높은 비충돌 사이클을 선택하는 식으로 진행합니다. 전역 최적은 아니지만 O(N * P/64) 시간에 실행됩니다. 실무에서 그리디 해는 최적의 5-10% 이내입니다.
패킹 후 재시뮬레이션
번들을 선택한 후, 순서대로 전체 시퀀스를 재시뮬레이션합니다. 첫 번째 사이클은 현재 상태에 대해 실행됩니다. 두 번째 사이클은 첫 번째 사이클 실행 후의 상태에 대해 시뮬레이션되어야 합니다:
pub fn validate_bundle(
simulator: &mut Simulator,
bundle: &[CycleWithMask],
) -> Vec<ValidatedTx> {
let mut validated = Vec::new();
let snapshot = simulator.snapshot();
for entry in bundle {
let calldata = build_execution_calldata(
&entry.cycle,
entry.cycle.optimal_input,
);
let result = simulator.simulate_swap(
bot_address(),
bot_contract(),
calldata.clone(),
U256::ZERO,
);
match result {
Ok(sim) if sim.success => {
validated.push(ValidatedTx {
calldata,
gas_used: sim.gas_used,
expected_profit: entry.cycle.expected_profit,
});
}
_ => {
tracing::debug!(
"사이클 {:?}이 번들 검증 중 실패",
entry.cycle.cycle.id
);
}
}
}
simulator.restore(snapshot);
validated
}
개별적으로 수익성이 있던 사이클이 번들 검증 중 실패할 수 있습니다. 번들의 이전 사이클이 의존하는 풀 상태를 변경했기 때문입니다.
번들 제출
Flashbots 번들
표준 제출 경로입니다. 번들은 Flashbots 호환 빌더에 제출되는 서명된 트랜잭션의 정렬된 목록입니다:
use ethers_flashbots::{FlashbotsMiddleware, BundleRequest};
pub async fn submit_flashbots_bundle(
client: &FlashbotsMiddleware<Provider<Http>, LocalWallet>,
transactions: Vec<Bytes>,
target_block: u64,
) -> Result<BundleHash> {
let mut bundle = BundleRequest::new();
for tx in transactions {
bundle = bundle.push_transaction(tx);
}
bundle = bundle
.set_block(target_block)
.set_simulation_block(target_block - 1)
.set_simulation_timestamp(0);
let result = client
.send_bundle(&bundle)
.await?;
Ok(result.bundle_hash)
}
Flashbots 번들은 핵심 속성을 가집니다: 전부 또는 전무 실행. 번들의 모든 트랜잭션이 성공하거나, 아무것도 포함되지 않습니다. 첫 번째 스왑은 성공하지만 두 번째가 되돌리는 부분 실행을 방지하여, 원치 않는 중간 토큰을 남기는 것을 막습니다.
MEV-Share
MEV-Share는 MEV를 사용자에게 재분배하는 Flashbots의 프로토콜입니다. 사용자가 Flashbots Protect를 통해 트랜잭션을 제출하면, 서처가 백런에 입찰할 수 있습니다. 사용자는 MEV의 일부를 받습니다.
직접 빌더 API
일부 빌더는 Flashbots를 우회하여 번들을 직접 수락합니다:
- Titan Builder:
https://rpc.titanbuilder.xyz - BeaverBuild:
https://rpc.beaverbuild.org - Rsync Builder:
https://rsync-builder.xyz
pub async fn submit_to_builders(
bundles: &[SignedBundle],
builders: &[BuilderEndpoint],
) {
// 모든 빌더에 동시 제출
let futures: Vec<_> = builders
.iter()
.map(|builder| {
submit_bundle_to_builder(builder, bundles)
})
.collect();
let results = futures::future::join_all(futures).await;
for (builder, result) in builders.iter().zip(results) {
match result {
Ok(_) => tracing::info!("{}에 제출됨", builder.name),
Err(e) => tracing::warn!("{} 실패: {}", builder.name, e),
}
}
}
항상 여러 빌더에 제출하세요. 블록 포함을 위해 경쟁하고 있습니다 — 더 많은 빌더가 번들을 볼수록 포함 확률이 높습니다.
Coinbase 전송: 빌더에 지불
MEV 번들은 가스 가격으로 포함 비용을 지불하지 않습니다. 대신 번들의 마지막 트랜잭션이 block.coinbase(빌더의 수수료 수신자)에 직접 ETH 전송을 보냅니다:
#define macro PAY_COINBASE() = takes(1) returns(0) {
// takes: [payment_amount]
0x00 0x00 0x00 0x00 // retSize, retOff, argSize, argOff
swap4 // [amount, 0, 0, 0, 0]
coinbase // [coinbase, amount, 0, 0, 0, 0]
gas call // [success]
pop
}
지불 금액은 일반적으로 예상 이익의 90-99%입니다. 서처는 1-10%를 마진으로 유지합니다. 더 높은 지불은 포함 확률을 높이지만 이익을 줄입니다. 이것은 끊임없는 게임 이론적 균형입니다.
엔드투엔드 흐름
모든 것을 종합하면, 프로덕션 MEV 봇의 블록당 흐름입니다:
1. 새 블록 도착 (t=0)
2. 이벤트에서 풀 준비금 업데이트 (t=0-500ms)
3. 사이클 인덱스를 통해 영향 받은 사이클 식별 (t=500ms-1s)
4. 영향 받은 사이클 + 순환 배치 시뮬레이션 (t=1-4s)
- 최적 입력 이진 검색
- 마감 시간 인식: 가장 높은 이익 먼저
5. 비트마스크 구축, 충돌 없는 번들 패킹 (t=4-5s)
6. 순서대로 번들 재시뮬레이션 (t=5-6s)
7. 트랜잭션 서명, coinbase 지불 계산 (t=6-7s)
8. Flashbots + 직접 빌더에 제출 (t=7-8s)
9. 포함 대기 (t=8-12s)
10. 결과 기록, 이익 추적 업데이트 (t=12s)
전체 파이프라인이 12초 블록 시간 내에 실행됩니다. 모든 밀리초가 중요합니다.
모니터링과 관찰 가능성
프로덕션 MEV 봇에는 포괄적인 모니터링이 필요합니다:
#[derive(Default)]
pub struct BlockMetrics {
pub cycles_evaluated: u64,
pub cycles_profitable: u64,
pub bundles_submitted: u64,
pub bundles_included: u64,
pub total_profit_wei: U256,
pub total_gas_cost_wei: U256,
pub simulation_time_ms: u64,
pub deadline_expired: bool,
}
포함률(bundles_included / bundles_submitted), 포함된 번들당 평균 이익, 마감 시간 미스를 추적합니다. 건강한 봇은 20% 이상의 포함률과 5% 미만의 마감 시간 미스를 가집니다.
요약
번들링은 개별 차익거래 트랜잭션을 블록당 이익을 최대화하는 최적화된 충돌 없는 패키지로 변환합니다. 비트마스크 충돌 감지는 마이크로초 내에 실행됩니다. 그리디 패킹은 거의 최적의 번들을 찾습니다. 재시뮬레이션은 현실적인 상태에 대해 번들을 검증합니다. 다중 빌더 제출은 포함 확률을 최대화합니다. 이것으로 Deep EVM 시리즈를 마무리합니다 — Huff의 원시 옵코드부터 프로덕션 MEV 인프라까지. 스택: 온체인 실행을 위한 Huff 컨트랙트, 오프체인 시뮬레이션 및 번들링을 위한 Rust, 공정한 블록 포함을 위한 Flashbots 생태계.