Мультичейн-платформа: криптоплатежи и кастоди
Приём и выплата криптовалют в сетях EVM, Tron и TON: ключи живут в изолированном подписанте, а каждое движение денег — устойчивая сага.
- Область
- Финтех, криптокастоди
- Наша роль
- Архитектура, бэкенд, криптография, фронтенд, DevOps
- Год
- 2023-2026
Мы построили для крупной массовой потребительской платформы бэкенд приёма платежей и хранения криптовалют в сетях EVM, Tron и TON. Он выдаёт адреса для депозитов, находит входящие платежи, собирает средства на горячий кошелёк, проводит выводы и сообщает о событиях бизнес-бэкенду через вебхуки с подписью; депозиты, сборы и выводы — саги на Temporal, доступ — через Keycloak. Система прошла два поколения: в продакшене ключи хранятся в изолированном HD-подписанте за узким интерфейсом подписи, а подписант на пороговой ECDSA (MPC) находится в пилоте.
Задача
- Принимать и выплачивать стейблкоины и нативные монеты в нескольких сетях — EVM, Tron и TON, — у каждой из которых свой формат адресов, стандарт токенов и модель комиссий, без собственной крипто-команды.
- Выдавать каждому пользователю свой адрес депозита, надёжно находить платежи и сообщать о каждом ровно один раз.
- Дёшево собирать средства с тысяч адресов, хотя на самих адресах нет ни монет на комиссию, ни сетевых ресурсов.
- Исключить двойную отправку при выводе, даже если процесс упал посреди трансляции транзакции.
- Дать операторам управлять системой без правки конфигов и доступа к ключам, а ключи хранить так, чтобы ни один процесс, база или оператор не мог вывести средства в одиночку.
Решение
Первое поколение (2023–2024) — набор контейнерных сервисов на Python: мультитенантный шлюз с API-ключами и колбэками для каждого клиента, подключаемые адаптеры для EVM, Tron и TON, парсеры переводов ERC-20, TRC-20 и TON-жетонов, очередь колбэков с повторами. Оно решило задачу процессинга, но в эксплуатации стало видно, что кастоди требует более строгих гарантий, и во втором поколении (2026) мы переписали систему с нуля на трёх принципах. Ключи изолированы: API и сканер знают только расширенный публичный ключ, а подписант принимает дайджест и возвращает подпись. Каждое движение денег — сага в Temporal с идемпотентными шагами, поэтому после сбоя на любом шаге процесс безопасно продолжается. Источник истины по балансам — сам блокчейн: система хранит только факты и статусы транзакций, без изменяемых колонок баланса. Итог — четыре компонента: кастоди-ядро, REST API под защитой Keycloak, консоль оператора на React и подписант.
Архитектура
Что умеет система
- Адреса из публичного ключа
- Адреса депозитов выводятся из расширенного публичного ключа по BIP-32/44 и привязываются к пользователям через внешние идентификаторы.
- Автоматический сбор средств
- Средства уходят на горячий кошелёк по одной из трёх стратегий оплаты комиссий: платить за каждый сбор, делегировать застейканные ресурсы сети или арендовать ресурсы на внешней площадке с автоматическим откатом.
- Выводы с лимитами
- Ключи идемпотентности, лимиты на транзакцию и скользящие суточные лимиты, проверка ликвидности горячего кошелька с учётом транзакций в пути.
- Интеграция через вебхуки
- События deposit.detected, sweep.done, withdrawal.confirmed и withdrawal.failed подписываются HMAC и доставляются с повторами; для отладки есть тестовая отправка.
- Консоль оператора
- Адреса с живыми балансами, депозиты, сборы, выводы, кошельки, настройки активов и колбэков, состояние подписанта. Роли viewer, operator и admin через Keycloak.
- Настройки без перезапуска
- RPC-провайдеры, активы, лимиты и колбэки хранятся в базе и применяются за секунды; переключение между тестовой и основной сетью — в один клик.
Инженерные решения
- Подписант заблокирован по умолчанию: после старта администратор вводит seed через консоль, система сверяет его с сохранённым публичным ключом и держит только в памяти — секреты не попадают ни в файлы, ни в переменные окружения, ни в базу, ни в историю workflow.
- Подписант на пороговой ECDSA (в пилоте) написан на Rust на протоколах семейства CGGMP: подпись идёт при ответе кворума, узел-нарушитель вычисляется и исключается, доли ключа периодически обновляются без смены публичного ключа и адресов. Переход на него — смена конфигурации, а не кода.
- Защита от двойной отправки: каждая транзакция сохраняет свой txid до трансляции, а при повторе система ищет квитанцию вместо новой отправки.
- Сканер читает только финализированные блоки и записывает депозиты вместе с курсором в одной транзакции базы, а саги запускаются с детерминированным id — повторно прочитанный блок не создаст дубль.
- Стратегию оплаты комиссий выбрали по замерам в тестовой сети: предсказуемая плата за каждый сбор вместо замороженного в стейкинге капитала, остальные стратегии — за переключателем для больших объёмов.
Интерфейс
Схемы
Стек
- Python 3.12
- FastAPI
- SQLAlchemy 2 (async)
- PostgreSQL
- Temporal
- web3.py
- tronpy
- TON SDKs
- Rust
- tonic gRPC + mTLS
- CGGMP threshold ECDSA
- Keycloak
- React 19 + TypeScript
- Docker
Что это показывает
- Проектируем мультичейн-кастоди (EVM, Tron, TON), где ключи изолированы за узким и проверяемым интерфейсом подписи.
- Применяем прикладную криптографию: HD-деривацию и пороговую ECDSA с обновлением долей и шифрованием их при хранении.
- Строим движение денег с гарантией «ровно один раз» на сагах и идемпотентных побочных эффектах.
- Развиваем системы в продакшене: переносим уроки первого поколения во второе, не таща за собой лишнюю сложность.
Вопросы по кейсу
Где хранятся приватные ключи?
В изолированной зоне подписи. В продакшене это HD-подписант: он стартует заблокированным, seed вводит администратор, и хранится он только в памяти. API и сканер работают с публичным ключом, а за границу зоны выходят только дайджесты и подписи. Подписант на пороговой ECDSA, где каждый узел держит лишь свою долю ключа, сейчас в пилоте.
Что будет, если процесс упадёт посреди вывода?
Вывод — это сага в Temporal, она продолжится с того же шага. Txid сохраняется до трансляции, поэтому при повторе система проверяет квитанцию в сети, а не отправляет транзакцию заново.
Как платформа интегрируется с нашим бэкендом?
Запросы идут через REST API с сервисным аккаунтом Keycloak (client credentials), события приходят вебхуками с подписью HMAC и повторами до успешного ответа. Выводы принимают ключ идемпотентности, так что повторный запрос не создаст второй вывод.