Deep EVM #15: Simulación MEV — Búsqueda Binaria, State Forks y el Deadline de 12 Segundos
Engineering Team
La necesidad de velocidad
En MEV, tienes exactamente 12 segundos entre bloques para detectar oportunidades, simular transacciones, optimizar montos y enviar bundles. Un motor de simulación lento significa oportunidades perdidas.
La cadena de ejecución típica:
- Nuevo bloque llega (t=0)
- Actualizar estado local (t=0.1s)
- Detectar oportunidades (t=0.5s)
- Simular y optimizar (t=1-8s)
- Construir y enviar bundles (t=8-10s)
- Buffer para propagación (t=10-12s)
La simulación consume la mayor parte del tiempo. Optimizarla es la diferencia entre capturar $10K o $0.
State forks con revm
revm es una implementación de la EVM en Rust, optimizada para simulación. Permite crear forks del estado de Ethereum y ejecutar transacciones hipotéticas sin afectar el estado real.
use revm::{EVM, db::CacheDB, primitives::*};
fn simulate_swap(
db: &CacheDB<EmptyDB>,
router: Address,
calldata: Bytes,
value: U256,
) -> Result<ExecutionResult, EvmError> {
let mut evm = EVM::new();
evm.database(db.clone());
evm.env.tx.caller = searcher_address();
evm.env.tx.transact_to = TransactTo::Call(router);
evm.env.tx.data = calldata;
evm.env.tx.value = value;
evm.env.tx.gas_limit = 500_000;
evm.transact()
}
Búsqueda binaria para monto óptimo
Dado un ciclo de arbitraje, necesitamos encontrar el monto de entrada que maximiza el beneficio. La función beneficio es típicamente cóncava (sube, alcanza un pico, y baja), lo que permite búsqueda ternaria:
fn find_optimal_amount(
db: &CacheDB<EmptyDB>,
cycle: &ArbCycle,
gas_cost: U256,
) -> (U256, U256) {
let mut lo = eth(0.01); // Mínimo: 0.01 ETH
let mut hi = eth(100.0); // Máximo: 100 ETH
for _ in 0..40 {
let mid1 = lo + (hi - lo) / 3;
let mid2 = hi - (hi - lo) / 3;
let profit1 = simulate_cycle(db, cycle, mid1)
.saturating_sub(gas_cost);
let profit2 = simulate_cycle(db, cycle, mid2)
.saturating_sub(gas_cost);
if profit1 < profit2 {
lo = mid1;
} else {
hi = mid2;
}
}
let optimal = (lo + hi) / 2;
let profit = simulate_cycle(db, cycle, optimal)
.saturating_sub(gas_cost);
(optimal, profit)
}
Pipeline de ejecución paralela
Con miles de oportunidades potenciales por bloque, la simulación secuencial no es suficiente. Usamos rayon para paralelizar:
use rayon::prelude::*;
fn evaluate_all_cycles(
db: &CacheDB<EmptyDB>,
cycles: &[ArbCycle],
gas_price: U256,
) -> Vec<(ArbCycle, U256, U256)> {
cycles.par_iter()
.filter_map(|cycle| {
let gas_cost = estimate_gas(cycle) * gas_price;
let (amount, profit) = find_optimal_amount(
db, cycle, gas_cost
);
if profit > min_profit_threshold() {
Some((cycle.clone(), amount, profit))
} else {
None
}
})
.collect()
}
CacheDB: evitar lecturas redundantes
Cada SLOAD que va al disco es lento. CacheDB cachea los valores leídos:
struct CacheDB<DB> {
accounts: HashMap<Address, AccountInfo>,
storage: HashMap<(Address, U256), U256>,
underlying: DB,
}
Cuando simulamos múltiples ciclos que comparten pools, la caché se llena con la primera simulación y las siguientes son significativamente más rápidas.
Manejo del deadline
Con 12 segundos por bloque, necesitamos un sistema de deadlines:
async fn mev_loop(deadline: Instant) {
let cycles = find_cycles().await;
let mut best = None;
for cycle in cycles {
// Verificar si queda tiempo
if Instant::now() > deadline - Duration::from_secs(2) {
break; // Reservar 2s para envío
}
let result = simulate(cycle).await;
if result.profit > best.map_or(U256::zero(), |b| b.profit) {
best = Some(result);
}
}
if let Some(best) = best {
submit_bundle(best).await;
}
}
Métricas de rendimiento
| Operación | Tiempo típico |
|---|---|
| Fork de estado | 1-5ms |
| Simulación de swap | 0.5-2ms |
| Búsqueda binaria (40 iter) | 20-80ms |
| Evaluación de 1000 ciclos (paralela) | 200-500ms |
| Construcción de bundle | 1-5ms |
| Envío a builder | 10-50ms |
Total: ~300-600ms para evaluar y enviar, dejando suficiente margen dentro del deadline de 12 segundos.
Conclusión
La simulación MEV es un problema de ingeniería de rendimiento. revm proporciona una EVM rápida para forks de estado, la búsqueda ternaria encuentra montos óptimos eficientemente, y la paralelización con rayon escala la evaluación de miles de ciclos. El manejo correcto del deadline de 12 segundos es la diferencia entre un bot rentable y uno que pierde oportunidades sistemáticamente.