Разработка системы индексации блокчейн-событий

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

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

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

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1309
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1222
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    922
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1151
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    614
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    887

Разработка системы индексации блокчейн-событий

Проблема появляется тогда, когда вам нужно ответить на вопрос «покажи все транзакции этого пользователя за последние 30 дней» или «какой общий объём торгов прошёл через наш DEX». Прямые вызовы к ноде через eth_getLogs с диапазоном блоков — это не архитектура, это костыль. На архивной ноде с блоком от 0 такой запрос займёт минуты, на публичном RPC — скорее всего упадёт с timeout или block range too large.

Правильная система индексации — это pipeline: нода → event listener → parser → база данных → API. Каждый компонент с независимым масштабированием и гарантией exactly-once или at-least-once семантики.

Архитектура event pipeline

Слой получения событий

Есть три подхода к получению событий из ноды, и у каждого своя цена надёжности:

Polling через eth_getLogs — самый простой и самый надёжный при правильной реализации. Воркер периодически запрашивает события за диапазон блоков, сохраняет lastIndexedBlock, при перезапуске продолжает с последнего обработанного блока.

interface IndexerState {
    lastIndexedBlock: number
    lastIndexedBlockHash: string // для детекции реорга
}

async function pollEvents(
    provider: ethers.JsonRpcProvider,
    contracts: ContractConfig[],
    state: IndexerState
): Promise<ProcessedEvent[]> {
    const currentBlock = await provider.getBlockNumber()
    const toBlock = Math.min(state.lastIndexedBlock + BATCH_SIZE, currentBlock - CONFIRMATIONS)

    if (toBlock <= state.lastIndexedBlock) return []

    // Верификация непрерывности chain
    const lastBlock = await provider.getBlock(state.lastIndexedBlock)
    if (lastBlock?.hash !== state.lastIndexedBlockHash) {
        throw new ReorgDetectedError(state.lastIndexedBlock)
    }

    const logs = await provider.getLogs({
        address: contracts.map(c => c.address),
        topics: [contracts.flatMap(c => c.topics)],
        fromBlock: state.lastIndexedBlock + 1,
        toBlock,
    })

    return logs.map(parseLog)
}

WebSocket subscriptions — низкая latency (новые события приходят немедленно), но WebSocket соединения нестабильны. Нужен automatic reconnect с backoff и синхронизация пропущенных блоков при переподключении:

async function subscribeWithFallback(provider: ethers.WebSocketProvider) {
    const filter = { address: CONTRACT, topics: [EVENT_TOPIC] }

    provider.on(filter, async (log) => {
        await processLog(log)
    })

    provider.websocket.on('close', async () => {
        console.log('WebSocket closed, syncing missed blocks...')
        await syncMissedBlocks(lastProcessedBlock)
        reconnect() // exponential backoff
    })
}

Firehose / StreamingFast — enterprise-уровень. Бинарный стриминг блоков со всем содержимым, минимальная latency, встроенная обработка реоргов. Используется как источник данных для The Graph protocol nodes. Существенно сложнее в настройке.

Обработка реорганизаций

Реорги — наиболее частый источник багов в индексерах. На Ethereum finality через PoS — после двух эпох (~12-13 минут) блок финален. На PoW сетях и молодых EVM цепочках (BSC, Polygon) реорги глубиной 3-5 блоков — норма.

Стратегия: индексировать с задержкой N блоков (safe confirmations), хранить hash каждого проиндексированного блока, при несоответствии — откат до последнего консистентного состояния.

-- Таблица состояния индексера
CREATE TABLE indexer_blocks (
    block_number    BIGINT PRIMARY KEY,
    block_hash      VARCHAR(66) NOT NULL,
    indexed_at      TIMESTAMPTZ DEFAULT NOW()
);

-- Событие с привязкой к блоку для возможности rollback
CREATE TABLE indexed_events (
    id              BIGSERIAL PRIMARY KEY,
    block_number    BIGINT NOT NULL REFERENCES indexer_blocks(block_number),
    log_index       INT NOT NULL,
    tx_hash         VARCHAR(66) NOT NULL,
    contract_addr   VARCHAR(42) NOT NULL,
    event_name      VARCHAR(100) NOT NULL,
    decoded_data    JSONB NOT NULL,
    UNIQUE(tx_hash, log_index)
);

При детекции реорга — DELETE FROM indexed_events WHERE block_number >= reorg_depth, затем переиндексация с реорговой точки.

Декодирование событий

ABI-декодирование событий — тривиальная задача для ethers.js или viem, но есть нюансы.

Indexed vs non-indexed параметры: indexed параметры попадают в topics, non-indexed — в data. Событие с 3 indexed параметрами + event signature занимает 4 topics. Декодирование topics для non-primitive типов (structs, dynamic arrays) невозможно — они хешируются через keccak256 и теряют исходные данные.

import { decodeEventLog } from 'viem'

function parseSwapEvent(log: Log, abi: Abi): SwapEvent {
    const decoded = decodeEventLog({
        abi,
        eventName: 'Swap',
        data: log.data,
        topics: log.topics,
    })

    return {
        blockNumber: log.blockNumber,
        txHash: log.transactionHash,
        sender: decoded.args.sender,
        recipient: decoded.args.recipient,
        amount0: decoded.args.amount0,
        amount1: decoded.args.amount1,
        sqrtPriceX96: decoded.args.sqrtPriceX96,
        liquidity: decoded.args.liquidity,
        tick: decoded.args.tick,
    }
}

Анонимные события (без event selector в topic0) — редкость, но встречается в старых контрактах. Декодирование требует знания структуры данных без ABI.

Proxy контракты: события эмитятся с адреса proxy, но ABI реализации. Нужно резолвить implementation address через EIP-1967 storage slot: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc.

База данных и производительность

Для хранения событий PostgreSQL — стандарт де-факто. Ключевые решения по схеме:

Партиционирование по block_number: для контрактов с высоким объёмом событий (Uniswap V3, крупные ERC-20) таблица быстро вырастает до сотен миллионов строк. Партиционирование по диапазону блоков даёт плановые сканы вместо seq scan:

CREATE TABLE swap_events (
    block_number BIGINT NOT NULL,
    -- ...остальные поля
) PARTITION BY RANGE (block_number);

CREATE TABLE swap_events_0_5m    PARTITION OF swap_events FOR VALUES FROM (0) TO (5000000);
CREATE TABLE swap_events_5m_10m  PARTITION OF swap_events FOR VALUES FROM (5000000) TO (10000000);
-- и т.д.

JSONB для decoded_data: хранение декодированных данных в JSONB позволяет добавлять новые типы событий без миграций схемы. Индексы GIN на часто запрашиваемые поля внутри JSONB. Для критичных полей — вынесение в отдельные типизированные колонки.

TimescaleDB — расширение для PostgreSQL, дающее автоматическое time-based партиционирование (hypertables), сжатие старых данных и continuous aggregates для OHLCV/метрик без фоновых джобов:

-- Continuous aggregate: volume per hour per pool
CREATE MATERIALIZED VIEW pool_hourly_volume
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', event_timestamp) AS hour,
    pool_address,
    SUM(amount_usd) AS volume_usd,
    COUNT(*) AS tx_count
FROM swap_events
GROUP BY 1, 2;

SELECT add_continuous_aggregate_policy('pool_hourly_volume',
    start_offset => INTERVAL '3 hours',
    end_offset   => INTERVAL '1 hour',
    schedule_interval => INTERVAL '1 hour'
);

Мониторинг и алерты

Индексер должен иметь метрики: indexer_lag_blocks (разрыв между head и последним проиндексированным блоком), events_per_second, reorg_count. Алерт если indexer_lag_blocks > 50 — индексер отстаёт или завис.

Деплой через Docker Compose / Kubernetes с health check endpoint, который возвращает 503 если lag превышает порог. Это позволяет load balancer-у исключить нездоровый инстанс.

Типичный стек: Go или Rust для воркера (производительность), PostgreSQL / TimescaleDB для хранения, Redis для state и очереди, Grafana + Prometheus для мониторинга.