Перейти к содержанию

Exchange Engine

Назначение

Exchange Engine — ядро торговой платформы Univex: матчинг ордеров, обработка депозитов и выводов средств, ведение клиентских балансов.

Язык: Go 1.25. Исходный код — в директории /exchange/exchange/.


Архитектура

Exchange Engine построен по микросервисной архитектуре. Каждый сервис запускается как отдельный процесс и взаимодействует с остальными через gRPC или очереди сообщений. Внешние клиенты работают с платформой через HTTP REST API или WebSocket.

┌─────────────────────────────────────────────────────────────┐
│                        Клиенты                              │
│         (браузер, мобильное приложение, торговый бот)       │
└───────────────────────────┬─────────────────────────────────┘
                            │ HTTP / WebSocket
                ┌───────────▼───────────┐
                │        api            │
                │  (HTTP REST + WS)     │
                └───────────┬───────────┘
                            │ gRPC (Unix socket)
                ┌───────────▼───────────┐
                │       engine          │
                │  (L3 orderbook,       │
                │   order matching)     │
                └───────────┬───────────┘
                            │ PostgreSQL
          ┌─────────────────┼─────────────────┐
          │                 │                 │
 ┌────────▼───────┐ ┌───────▼──────┐ ┌───────▼──────────┐
 │  hotmanager    │ │  topupscan   │ │   txprocessor    │
 │ (BTC/EVM/Tron) │ │(BTC/EVM/Tron)│ │ (BTC/EVM/Tron)   │
 └────────────────┘ └──────────────┘ └──────────────────┘
 ┌────────▼───────┐
 │     admin      │
 │ (gRPC, 11      │
 │  sub-сервисов) │
 └────────────────┘

Принципы проектирования

  • Разделение ответственности — каждый сервис выполняет строго определённую функцию.
  • Движок — торговый движок использует L3 книгу ордеров в памяти и общается с API через Unix-сокет.
  • Поддержка нескольких блокчейнов — блокчейн-зависимые сервисы реализованы отдельно для BTC, EVM-совместимых сетей и Tron.
  • Наблюдаемость — алерты и статусы хранятся в базе данных; предусмотрены gRPC-стримы для мониторинга в реальном времени.

Список сервисов

Сервис Директория Протокол Назначение
engine cmd/engine gRPC + Unix socket Торговый движок, L3 книга ордеров, матчинг
api cmd/api HTTP REST + WebSocket Внешний API для клиентов
admin cmd/admin gRPC Административный API (11 sub-сервисов)
hotmanager cmd/hotmanager/{btc,evm,tron} внутренний Управление горячими кошельками
topupscan cmd/topupscan/{btc,evm,tron,...} внутренний Сканирование депозитов
txprocessor cmd/txprocessor/{btc,evm,tron} внутренний Обработка выводов и транзакций
migrator cmd/migrator CLI Применение миграций базы данных
backup_wallets cmd/backup_wallets CLI (one-shot) Миграция существующих кошельков из PostgreSQL в S3

Структура внутренних пакетов

internal/ — внутренние пакеты

internal/
├── engine/          # Торговый движок: матчинг ордеров, управление книгой заявок
│   ├── service.go   # Основной gRPC-сервис с AddOrder, CancelOrder, подписками
│   ├── orders.go    # Логика размещения и отмены ордеров
│   └── market_data.go # Агрегация рыночных данных и стриминг
├── api/             # HTTP REST + WebSocket API
│   ├── service.go   # Настройка Echo-сервера и регистрация маршрутов
│   ├── accounts.go  # Регистрация, логин, управление аккаунтами
│   ├── orders.go    # Размещение, отмена и просмотр ордеров
│   ├── orders_ws.go # WebSocket-стрим обновлений ордеров
│   ├── balances.go  # Запросы балансов и внутренние переводы
│   ├── market_data.go # WS-стримы orderbook и trades
│   ├── trades.go    # Публичные и пользовательские сделки
│   ├── klines.go    # OHLC-свечи (candlestick)
│   ├── books.go     # Запросы стакана заявок
│   ├── auth.go      # JWT и HMAC-SHA256 middleware
│   └── ratelimit.go # Rate limiting (IP + per-account tier)
├── admin/           # gRPC-обработчики администрирования (11 sub-сервисов)
│   ├── accounts_service.go
│   ├── blockchain_service.go
│   ├── operations_service.go
│   ├── hotwallets_service.go
│   ├── alerts_service.go
│   ├── apikeys_service.go
│   ├── assets_service.go
│   ├── tickers_service.go
│   ├── tiers_service.go
│   ├── info_service.go
│   └── ping_service.go
├── auth/            # Аутентификация: gRPC-интерсепторы для извлечения account_id
├── bunmodels/       # ORM-модели (Bun): 28+ сущностей с автошифрованием
├── candles/         # Агрегация OHLC-свечей из потока сделок
├── repository/      # Слой доступа к данным (wallet, UTXO)
├── topupscan/       # Сканирование блокчейнов на входящие депозиты
│   ├── bitcoin.go   # Мониторинг BTC-адресов, UTXO-скан
│   ├── evm.go       # Мониторинг EVM-цепей (event logs)
│   ├── tron.go      # Мониторинг Tron-транзакций
│   ├── processor.go # Обработка подтверждённых депозитов
│   ├── aml_poller.go # Опрос Elliptic API для AML-скоринга
│   └── refund_processor.go # Возврат грязных депозитов
├── txprocessor/     # Исполнение исходящих транзакций (выводы)
│   ├── btc.go       # Построение BTC-транзакций из UTXO
│   ├── evm.go       # EVM-транзакции с оценкой gas
│   └── tron.go      # TRC-20 / TRX переводы
├── hotmanager/      # Управление горячими кошельками по сетям
│   ├── btc.go, evm.go, tron.go
│   ├── oracle.go    # Интеграция с ценовым оракулом
│   └── db_assets.go # Конфигурация активов из БД
└── tracext/         # Настройка OpenTelemetry трейсинга

pkg/ — реиспользуемые пакеты

pkg/
├── orderbook/       # L3 книга ордеров (красно-чёрные деревья)
│   ├── orderbook_l3.go  # Основная структура: asks/bids как RB-деревья
│   ├── quote_l3.go      # Уровень цены: связный список ордеров (FIFO)
│   ├── manager.go       # Потокобезопасный менеджер с ctxlock
│   ├── snapshotter.go   # Снимки книги для persistence
│   └── diff.go          # Расчёт дельт между состояниями
├── pubsub/          # Типизированный pub/sub с буферизованными каналами
├── cryptoext/       # AES-256-GCM шифрование приватных ключей в БД
├── priceoracle/     # Оракул цен активов (для балансировки кошельков)
├── walletutil/      # Валидация адресов и конвертация ключей (BTC/ETH/TRON)
├── btcext/          # Bitcoin: HD-кошельки, UTXO, адреса (P2PKH/P2SH/SegWit/Taproot)
├── ethext/          # Ethereum: клиент Web3, ERC-20 ABI, валидация адресов
│   └── abi/         # Сгенерированные ABI контрактов (ERC-20, TxProcessor)
├── tron/            # Tron: TRC-20, TRX-переводы, API-клиент
├── grpcext/         # gRPC: настройка сервера/клиента, трейсинг, пул соединений
├── httpext/         # HTTP: метрики, трейсинг, reason-коды, обработка ошибок
├── config/          # Конфигурация: MegaConfig, PostgreSQL, Redis, Engine-клиенты
├── pgext/           # PostgreSQL: глобальный ключ шифрования, хуки Bun ORM
├── tracing/         # OpenTelemetry: Tracer, Frame, span-теги
├── logging/         # Zap: логирование Bun-запросов и gRPC-вызовов
├── commission/      # Расчёт комиссий maker/taker
├── proto/           # Сгенерированный protobuf-код (admin, engine, blockchain)
├── ctxlock/         # Контекстно-зависимые блокировки (для L3 менеджера)
├── list/            # Дженерик-связный список для очередей ордеров
├── dirext/          # Направление ордера (BID/ASK) + компараторы цен
├── protoconv/       # Конвертация типов: decimal, UUID, time ↔ protobuf
├── statusext/       # gRPC-статусы с кастомными reason-кодами
├── instrument/      # Метаданные торговых пар (лимиты, шаги)
├── buildinfo/       # Версия и информация о сборке
├── baseconfig/      # Базовая конфигурация и парсинг опций
├── metricsext/      # Prometheus registry и хелперы
├── hawk/            # HAWK-аутентификация для API-ключей
├── stop/            # Graceful shutdown
├── admin/           # gRPC-клиент для Admin-сервиса
└── testutil/        # Тестовые утилиты

Хранилища данных

  • PostgreSQL — основная реляционная СУБД. Содержит 28 таблиц: учётные записи, балансы, ордера, сделки, транзакции, конфигурация активов и прочее. Подробнее — в разделе База данных.
  • In-memory L3 orderbook — книга ордеров хранится в оперативной памяти торгового движка для максимальной скорости матчинга.
  • S3-совместимое хранилище — объектное хранилище для зашифрованных резервных копий приватных ключей кошельков. Бакет: exchange-wallets-backup-local.

Шифрование и резервное копирование приватных ключей

Шифрование в базе данных (AES-256-GCM)

Все приватные ключи кошельков и секреты API-ключей хранятся в PostgreSQL исключительно в зашифрованном виде. Используется симметричное шифрование AES-256-GCM:

Параметр Значение
Алгоритм AES-256-GCM (симметричное шифрование)
Ключ 256 бит (32 байта), hex-encoded строка 64 символа
Nonce 12 байт, генерируется случайно при каждом шифровании
Формат хранения [nonce][GCM ciphertext] (nonce prepended к шифротексту)
Реализация pkg/cryptoext/aes.go

GCM обеспечивает одновременно конфиденциальность и аутентичность данных — любое повреждение шифротекста обнаруживается при расшифровке.

Шифрование и расшифровка выполняются автоматически на уровне ORM-хуков модели Bun:

  1. При записи в БД (BeforeAppendModel, INSERT/UPDATE) — поле PrivateKey шифруется и сохраняется в колонку encrypted_private_key BYTEA.
  2. При чтении из БД (AfterScanRow, SELECT) — колонка encrypted_private_key расшифровывается в поле PrivateKey.
  3. В памяти приложения ключ доступен в открытом виде, в базе данных — только в зашифрованном.

Тот же механизм применяется к API-ключам: поле Secret ↔ колонка encrypted_secret.

Мастер-ключ шифрования БД

Единый симметричный ключ передаётся сервисам через:

  • Переменную окружения POSTGRES_ENCRYPTION_KEY
  • Конфигурационный файл: поле postgres.encryption_key
  • Kubernetes Secret: exchange-db-encryption-key

При старте приложения ключ регистрируется в глобальном хранилище (pkg/pgext/encryption.go) и автоматически внедряется во все запросы к БД через EncryptionKeyHook.

Резервное копирование в S3 (RSA-OAEP + AES-256-GCM)

При создании каждого кошелька его данные автоматически копируются в S3-совместимое объектное хранилище (MinIO). В отличие от БД, для S3-бекапов используется асимметричное гибридное шифрование:

Параметр Значение
Бакет exchange-wallets-backup
Путь wallets/{network}/{wallet_id}.json
Шифрование RSA-OAEP (обёртка ключа) + AES-256-GCM (шифрование данных)
RSA-ключ RSA-4096
AES-ключ 256 бит, генерируется случайно для каждого кошелька
Wire-формат [2B key length][RSA-OAEP encrypted AES key][12B nonce][GCM ciphertext]
Реализация pkg/s3ext/client.go

Процесс шифрования при бекапе:

  1. Генерируется случайный AES-256 ключ (уникальный для каждого кошелька).
  2. Данные кошелька (JSON: id, network, private_key, address, account_id) шифруются AES-256-GCM.
  3. Случайный AES-ключ оборачивается RSA-OAEP с использованием публичного RSA-4096 ключа.
  4. Результат записывается в wire-формате и загружается в S3.

Для шифрования нужен только публичный ключ (безопасно хранится в конфигурации). Для расшифровки — приватный ключ (хранится отдельно).

Конфигурация S3

Параметр Описание
s3.bucket Имя бакета
s3.endpoint URL MinIO/S3 (напр. http://minio:9000)
s3.access_key_id Access key
s3.secret_access_key Secret key
s3.public_key_pem PEM-encoded RSA-4096 публичный ключ
s3.private_key_pem PEM-encoded RSA-4096 приватный ключ (опционально, только для восстановления)

Утилита бекапа существующих кошельков

cmd/backup_wallets — one-shot утилита для однократного бекапа всех кошельков из PostgreSQL в S3:

BACKUP_CONFIG=/config/config.yml VERIFY=true ./backup_wallets
  • Читает все кошельки с encrypted_private_key IS NOT NULL из БД
  • Расшифровывает через AES-256-GCM (мастер-ключ БД)
  • Зашифровывает через RSA-OAEP + AES-256-GCM и загружает в S3
  • С флагом VERIFY=true — после загрузки скачивает, расшифровывает и проверяет адрес

Процедура восстановления из PostgreSQL

При наличии доступа к БД и мастер-ключу шифрования:

  1. Получить мастер-ключ AES-256 из Kubernetes Secret exchange-db-encryption-key
  2. Прочитать encrypted_private_key из таблицы wallets
  3. Расшифровать через pkg/cryptoext.DecryptAES256(ciphertext, key) — формат: [12B nonce][GCM ciphertext]

Этот способ работает для всех кошельков, которые есть в базе данных.

Процедура восстановления из S3-бекапа

При утрате доступа к БД или необходимости восстановления из внешнего бекапа:

  1. Скачать файл wallets/{network}/{wallet_id}.json из бакета exchange-wallets-backup
  2. Получить приватный RSA-ключ из защищённого хранилища
  3. Распарсить wire-формат: первые 2 байта — длина зашифрованного AES-ключа (big-endian)
  4. Расшифровать AES-ключ через rsa.DecryptOAEP(sha256, privateKey, encryptedAESKey)
  5. Расшифровать данные: aesGCM.Open(nonce, ciphertext) с использованием расшифрованного AES-ключа
  6. Результат — JSON с полями id, network, private_key, address, account_id

Хранение ключей

Приватный RSA-ключ (для расшифровки S3-бекапов) должен храниться отдельно от инфраструктуры — на офлайн-носителе или в изолированном от кластера месте. Хранить его в Kubernetes или рядом с бекапами не имеет смысла: при компрометации инфраструктуры он не должен быть доступен. Мастер-ключ AES-256 (для БД) хранится в Kubernetes Secret exchange-db-encryption-key. Потеря любого из ключей делает расшифровку соответствующих данных невозможной.

Миграция с незашифрованных ключей

Исторически приватные ключи хранились в открытом виде. Миграция на зашифрованное хранение выполнена в два этапа:

  1. 20251221000000_encrypt_wallet_private_keys.go — батчевое шифрование всех существующих ключей (по 100 записей) и запись в новую колонку encrypted_private_key.
  2. 20251221000001_drop_old_private_key.go — удаление старой колонки private_key с открытыми ключами.

Аналогичная миграция для API-ключей: 20260309000000_encrypt_api_key_secrets.go.

При запуске миграций требуется переменная MIGRATOR_POSTGRES_ENCRYPTION_KEY.


Связанные разделы

  • Сервисы — детальное описание каждого сервиса
  • База данных — схема и описание таблиц
  • REST API — HTTP-эндпоинты для клиентов
  • Admin gRPC API — административный интерфейс
  • Engine gRPC API — внутренний gRPC-интерфейс торгового движка