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:
- При записи в БД (
BeforeAppendModel, INSERT/UPDATE) — полеPrivateKeyшифруется и сохраняется в колонкуencrypted_private_key BYTEA. - При чтении из БД (
AfterScanRow, SELECT) — колонкаencrypted_private_keyрасшифровывается в полеPrivateKey. - В памяти приложения ключ доступен в открытом виде, в базе данных — только в зашифрованном.
Тот же механизм применяется к 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 |
Процесс шифрования при бекапе:
- Генерируется случайный AES-256 ключ (уникальный для каждого кошелька).
- Данные кошелька (JSON: id, network, private_key, address, account_id) шифруются AES-256-GCM.
- Случайный AES-ключ оборачивается RSA-OAEP с использованием публичного RSA-4096 ключа.
- Результат записывается в 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:
- Читает все кошельки с
encrypted_private_key IS NOT NULLиз БД - Расшифровывает через AES-256-GCM (мастер-ключ БД)
- Зашифровывает через RSA-OAEP + AES-256-GCM и загружает в S3
- С флагом
VERIFY=true— после загрузки скачивает, расшифровывает и проверяет адрес
Процедура восстановления из PostgreSQL¶
При наличии доступа к БД и мастер-ключу шифрования:
- Получить мастер-ключ AES-256 из Kubernetes Secret
exchange-db-encryption-key - Прочитать
encrypted_private_keyиз таблицыwallets - Расшифровать через
pkg/cryptoext.DecryptAES256(ciphertext, key)— формат:[12B nonce][GCM ciphertext]
Этот способ работает для всех кошельков, которые есть в базе данных.
Процедура восстановления из S3-бекапа¶
При утрате доступа к БД или необходимости восстановления из внешнего бекапа:
- Скачать файл
wallets/{network}/{wallet_id}.jsonиз бакетаexchange-wallets-backup - Получить приватный RSA-ключ из защищённого хранилища
- Распарсить wire-формат: первые 2 байта — длина зашифрованного AES-ключа (big-endian)
- Расшифровать AES-ключ через
rsa.DecryptOAEP(sha256, privateKey, encryptedAESKey) - Расшифровать данные:
aesGCM.Open(nonce, ciphertext)с использованием расшифрованного AES-ключа - Результат — JSON с полями
id,network,private_key,address,account_id
Хранение ключей
Приватный RSA-ключ (для расшифровки S3-бекапов) должен храниться отдельно от инфраструктуры — на офлайн-носителе или в изолированном от кластера месте. Хранить его в Kubernetes или рядом с бекапами не имеет смысла: при компрометации инфраструктуры он не должен быть доступен. Мастер-ключ AES-256 (для БД) хранится в Kubernetes Secret exchange-db-encryption-key. Потеря любого из ключей делает расшифровку соответствующих данных невозможной.
Миграция с незашифрованных ключей¶
Исторически приватные ключи хранились в открытом виде. Миграция на зашифрованное хранение выполнена в два этапа:
20251221000000_encrypt_wallet_private_keys.go— батчевое шифрование всех существующих ключей (по 100 записей) и запись в новую колонкуencrypted_private_key.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-интерфейс торгового движка