Розробка бекенду dApp на Rust

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка бекенду dApp на Rust
Складний
~1-2 тижні
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1306
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1218
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    920
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1147
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    610
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    885

Розробка 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 генерація, верифікація підписів у hot path
  • Memory safety без GC пауз: DeFi бекенд не може дозволити собі 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, повна типізація.

Event індексер: підписка та обробка

Найпоширеніше завдання бекенду — слухати події контракту та оновлювати БД. На 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());

Робота з нодою: стійкість та 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 з'єднання до Ethereum нод замість HTTP (менше overhead)
  • jemalloc замість системного allocator'а для зменшення latency
  • CPU pinning через tokio::runtime::Builder::new_current_thread() для критичних шляхів
  • Flamegraph профілювання через cargo flamegraph перед оптимізацією
#[global_allocator]
static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;

Деплой

Rust бінарник — статично línkований виконавчий файл без залежностей. Docker образ важить 20-50MB проти 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) — мінімальна атакуємна поверхня.