بلوكتشينMar 28, 2026
Deep EVM #15: محاكاة MEV — البحث الثنائي وتفريعات الحالة والموعد النهائي 12 ثانية
OS
Open Soft Team
Engineering Team
لماذا المحاكاة ضرورية
لا يمكنك إرسال معاملة MEV إلى بلوكتشين “وتأمل الأفضل”. يجب أن تعرف مسبقاً:
- هل ستنجح المعاملة؟
- كم غاز ستستهلك؟
- ما الربح الدقيق؟
المحاكاة تنفذ المعاملة محلياً على نسخة من حالة البلوكتشين دون إرسالها فعلاً.
تفريع حالة EVM
نستخدم مكتبة revm (Rust EVM) لإنشاء بيئة تنفيذ محلية:
use revm::{Database, Evm, InMemoryDB};
fn simulate_swap(
db: &mut InMemoryDB,
router: Address,
calldata: Bytes,
) -> Result<SimResult> {
let mut evm = Evm::builder()
.with_db(db)
.with_tx_env(|tx| {
tx.caller = bot_address;
tx.transact_to = router;
tx.data = calldata;
tx.gas_limit = 500_000;
})
.build();
let result = evm.transact()?;
Ok(SimResult {
gas_used: result.gas_used,
output: result.output,
success: result.is_success(),
})
}
البحث الثنائي للمبلغ الأمثل
دالة الربح ليست خطية — هناك نقطة مثلى حيث الربح أقصى. البحث الثنائي يجدها بكفاءة:
async fn binary_search_optimal(
cycle: &Cycle,
db: &mut InMemoryDB,
) -> Result<(U256, U256)> {
let mut lo = parse_ether("0.01")?;
let mut hi = parse_ether("100")?;
let mut best_amount = lo;
let mut best_profit = U256::ZERO;
for iteration in 0..48 {
let mid = (lo + hi) / 2;
// محاكاة الدورة الكاملة
let db_fork = db.clone(); // تفريع الحالة
let result = simulate_cycle(&mut db_fork, cycle, mid).await?;
let profit = result.output.saturating_sub(mid);
if profit > best_profit {
best_profit = profit;
best_amount = mid;
}
// تحديد اتجاه البحث
let db_fork2 = db.clone();
let result_plus = simulate_cycle(&mut db_fork2, cycle, mid + parse_ether("0.001")?).await?;
let profit_plus = result_plus.output.saturating_sub(mid + parse_ether("0.001")?);
if profit_plus > profit {
lo = mid;
} else {
hi = mid;
}
}
Ok((best_amount, best_profit))
}
الموعد النهائي 12 ثانية
على إيثيريوم، كتلة جديدة كل 12 ثانية. هذا يعني:
- اكتشاف الفرصة
- محاكاة المعاملة
- تحسين المبلغ
- إرسال الحزمة
كل هذا يجب أن يحدث في أقل من 12 ثانية. عملياً، الهدف أقل من 500 ميلي ثانية.
use tokio::time::{timeout, Duration};
async fn process_opportunity(opp: Opportunity) -> Result<()> {
let deadline = Duration::from_millis(400);
let result = timeout(deadline, async {
let sim = simulate(&opp).await?;
if sim.profit > MIN_PROFIT {
let optimal = binary_search_optimal(&opp, &mut sim.db).await?;
submit_bundle(&opp, optimal).await?;
}
Ok::<_, Error>(())
}).await;
match result {
Ok(Ok(())) => {}
Ok(Err(e)) => tracing::warn!("فشلت المعالجة: {e}"),
Err(_) => tracing::warn!("تجاوز الموعد النهائي"),
}
Ok(())
}
تخبئة الحالة
إعادة تحميل حالة كل مجمع من RPC مكلف. بدلاً من ذلك:
- عند بداية الكتلة — تحميل احتياطيات جميع المجمعات المراقبة
- عند وصول معاملة معلقة — تطبيق التغييرات محلياً
- محاكاة — استخدام الحالة المحلية المحدثة
async fn update_reserves_on_new_block(
provider: &Provider,
pools: &mut HashMap<Address, Pool>,
) {
let multicall = Multicall::new(provider);
for pool in pools.values() {
multicall.add_call(pool.address, "getReserves()");
}
let results = multicall.execute().await?;
// تحديث الاحتياطيات محلياً
}
مقاييس الأداء
| العملية | الوقت النموذجي |
|---|---|
| تحميل الاحتياطيات (500 مجمع) | 50ms |
| DFS (1000 رمز) | 10ms |
| محاكاة دورة واحدة | 2ms |
| بحث ثنائي (48 تكرار) | 96ms |
| إرسال الحزمة | 20ms |
| الإجمالي | ~180ms |
هذا يترك هامشاً واسعاً ضمن الموعد النهائي 12 ثانية.
الخلاصة
طبقة المحاكاة هي الفرق بين روبوت MEV مربح وآخر خاسر. تفريع الحالة المحلي مع revm، البحث الثنائي للتحسين، والالتزام الصارم بالموعد النهائي هي المكونات الأساسية.