Разработка бэкенда dApp на Rust
Большинство dApp бэкендов пишут на Node.js — и это нормально до определённого масштаба. Но есть класс задач, где Rust не просто быстрее, а принципиально меняет что возможно: обработка миллионов событий из блокчейна в реальном времени, MEV боты с latency < 1ms, криптографические вычисления, парсинг и индексация on-chain данных. Это именно те случаи.
Когда Rust оправдан для dApp бэкенда
Не каждый dApp нуждается в Rust бэкенде. Node.js + TypeScript закрывает 80% случаев. Rust оправдан когда:
- Latency критична: MEV, arbitrage боты, ликвидации — миллисекунды стоят денег
- Throughput высокий: индексация сотен тысяч блоков, обработка event streams от нескольких нод
- Криптография: ZK-proof generation, верификация подписей в hot path
- Memory safety без GC пауз: DeFi backend не может позволить себе GC паузы в 50ms во время riskcheck
Стек: alloy + axum
alloy — современная Rust библиотека для Ethereum, замена устаревшего ethers-rs. Разработана той же командой, значительно лучше API:
[dependencies]
alloy = { version = "0.3", features = ["full"] }
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.5", features = ["cors", "trace"] }
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-tls"] }
use alloy::{
providers::{Provider, ProviderBuilder, WsConnect},
primitives::{address, U256},
sol,
};
// Генерируем типы из ABI на этапе компиляции
sol!(
#[allow(missing_docs)]
#[sol(rpc)]
ERC20,
"abi/ERC20.json"
);
#[tokio::main]
async fn main() -> eyre::Result<()> {
let ws = WsConnect::new("wss://eth-mainnet.g.alchemy.com/v2/KEY");
let provider = ProviderBuilder::new().on_ws(ws).await?;
let token = ERC20::new(address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), provider);
let balance = token.balanceOf(address!("...")).call().await?;
Ok(())
}
Ключевое преимущество sol! макроса — ABI encoding/decoding происходит на этапе компиляции, нет runtime overhead, полный type safety.
Event indexer: подписка и обработка
Самая частая задача бэкенда — слушать события контракта и обновлять БД. В Rust это делается чище, чем в любом другом языке:
use alloy::rpc::types::Filter;
use futures_util::StreamExt;
async fn index_transfers(
provider: Arc<impl Provider>,
db: Arc<PgPool>,
contract: Address,
from_block: u64,
) -> eyre::Result<()> {
let filter = Filter::new()
.address(contract)
.event("Transfer(address,address,uint256)")
.from_block(from_block);
let mut stream = provider.subscribe_logs(&filter).await?;
while let Some(log) = stream.next().await {
let transfer = ERC20::Transfer::decode_log(&log, true)?;
sqlx::query!(
"INSERT INTO transfers (tx_hash, from_addr, to_addr, amount, block_number)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (tx_hash) DO NOTHING",
log.transaction_hash.map(|h| h.to_string()),
transfer.from.to_string(),
transfer.to.to_string(),
transfer.value.to_string(), // U256 -> String для PostgreSQL numeric
log.block_number.map(|n| n as i64),
)
.execute(&*db)
.await?;
}
Ok(())
}
Backfill историческими данными — для индексации прошлых блоков используем get_logs с диапазонами блоков. Оптимальный chunk size — 2000 блоков (лимит большинства нод). Параллелим через tokio::spawn с семафором для контроля concurrency:
use tokio::sync::Semaphore;
let semaphore = Arc::new(Semaphore::new(10)); // 10 параллельных запросов
let tasks: Vec<_> = block_ranges.iter().map(|(from, to)| {
let permit = semaphore.clone().acquire_owned();
let provider = provider.clone();
tokio::spawn(async move {
let _permit = permit.await.unwrap();
fetch_and_index_range(provider, *from, *to).await
})
}).collect();
futures::future::join_all(tasks).await;
HTTP API с axum
use axum::{Router, routing::get, extract::{State, Path}, Json};
#[derive(Clone)]
struct AppState {
db: PgPool,
provider: Arc<dyn Provider>,
}
async fn get_token_balance(
State(state): State<AppState>,
Path((address, token)): Path<(String, String)>,
) -> Result<Json<BalanceResponse>, AppError> {
let addr: Address = address.parse()?;
let token_addr: Address = token.parse()?;
let contract = ERC20::new(token_addr, state.provider.clone());
let balance = contract.balanceOf(addr).call().await?;
Ok(Json(BalanceResponse {
address,
balance: balance.to_string(),
decimals: 18,
}))
}
let app = Router::new()
.route("/balance/:address/:token", get(get_token_balance))
.with_state(state)
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http());
Работа с нодой: resilience и failover
Production бэкенд не может зависеть от одной ноды. Реализуем retry логику и fallback:
use alloy::providers::fillers::{FillProvider, RecommendedFillers};
// Несколько RPC провайдеров с приоритетами
let providers = vec![
"wss://eth-mainnet.g.alchemy.com/v2/KEY1",
"wss://mainnet.infura.io/ws/v3/KEY2",
];
// При падении одного - автоматически переключаемся
// Реализуем через tower::retry middleware
Для высоконагруженных сценариев рекомендуем собственную Ethereum ноду (Erigon для архивных данных, Reth для скорости). Erigon синхронизируется быстрее Geth и потребляет значительно меньше места.
Криптографические операции
Rust + arkworks или halo2 для ZK-компонентов. Пример: верификация Groth16 proof на бэкенде перед отправкой транзакции:
use ark_groth16::{Groth16, Proof, VerifyingKey};
use ark_bn254::Bn254;
fn verify_proof(
vk: &VerifyingKey<Bn254>,
proof: &Proof<Bn254>,
public_inputs: &[Fr],
) -> bool {
Groth16::<Bn254>::verify(vk, public_inputs, proof)
.expect("Verification failed")
}
На Rust это работает в разы быстрее, чем snarkjs в Node.js.
MEV и latency оптимизация
Для MEV ботов каждая микросекунда важна:
- Используем raw TCP connections к Ethereum нодам вместо HTTP (меньше overhead)
- jemalloc вместо системного аллокатора для снижения latency
-
CPU pinning через
tokio::runtime::Builder::new_current_thread()для критических путей -
Flamegraph профилирование через
cargo flamegraphперед оптимизацией
#[global_allocator]
static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
Деплой
Rust бинарник — статически линкованный исполняемый файл без зависимостей. Docker образ весит 20-50MB vs 200MB+ для Node.js.
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/dapp-backend /usr/local/bin/
CMD ["dapp-backend"]
Для production используем distroless образы (gcr.io/distroless/cc) — минимальная атакуемая поверхность.







