Архитектура безопасности MERX: как мы защищаем ваши средства
Когда платформа обрабатывает финансовые транзакции, безопасность — это не функция, а основание, на котором всё держится. Единая уязвимость может навсегда разрушить доверие пользователей. В MERX соображения безопасности определили каждое архитектурное решение с самого начала, а не были добавлены как последующее расширение.
В этой статье описывается архитектура безопасности MERX: как защищаются средства, как обеспечивается целостность данных, как система защищается от типичных векторов атак и какие принципы проектирования делают платформу устойчивой.
Принцип 1: отсутствие хранения приватных ключей
MERX никогда не хранит, не сохраняет и не имеет доступа к вашим приватным ключам TRON. Это фундаментальное решение по проектированию, которое исключает целый класс атак.
Как это работает
Когда вы используете MERX для покупки energy, делегирование происходит с адреса провайдера на ваш адрес. MERX организует эту транзакцию, но никогда не нуждается в доступе к вашему кошельку. Ваш приватный ключ остаётся на вашем устройстве, в аппаратном кошельке или там, где вы его храните.
Процесс:
1. Вы говорите MERX: "Делегируй 65 000 energy на TMyAddress"
2. MERX говорит провайдеру: "Делегируй 65 000 energy на TMyAddress"
3. Провайдер делегирует с TProviderAddress на TMyAddress
4. MERX проверяет делегирование в блокчейне
5. Ваш приватный ключ никогда не был задействован
Почему это важно
Если бы MERX была скомпрометирована, злоумышленники не смогли бы украсть ваш TRX или токены, потому что MERX не имеет ваших ключей. Сравните это с платформами, которые требуют депозита токенов на контролируемый ими адрес — такие платформы хранят ваши ключи (или ключи от ваших средств), создавая единую точку отказа.
Исключение казначейства
MERX действительно управляет собственным адресом казначейства для получения депозитов и обработки вывода. Приватный ключ казначейства хранится как секрет Docker, доступный только сервису treasury-signer. Он никогда не раскрывается сервису API, веб-интерфейсу или любому другому компоненту. Подробнее об изоляции ниже.
Принцип 2: двойная бухгалтерская книга
Каждая финансовая операция на MERX создаёт парную запись в реестре. Это тот же принцип бухгалтерского учёта, используемый каждым банком и финансовым учреждением последние 700 лет. Это работает.
Как это работает
Каждая транзакция создаёт две записи: дебет и кредит. Сумма всех дебетов всегда равна сумме всех кредитов. Если они не совпадают, что-то не так, и система это немедленно обнаруживает.
-- Пример оплаты заказа
INSERT INTO ledger (account_id, type, amount_sun, direction)
VALUES
($user_id, 'ORDER_PAYMENT', 5525000, 'DEBIT'),
($provider_settlement, 'ORDER_PAYMENT', 5525000, 'CREDIT');
Неизменяемость
Записи реестра никогда не обновляются и не удаляются. Если транзакция должна быть отменена (например, возврат), создаётся новая запись реестра с противоположным направлением:
-- Возврат: новые записи, исходные остаются
INSERT INTO ledger (account_id, type, amount_sun, direction, reference_id)
VALUES
($user_id, 'REFUND', 5525000, 'CREDIT', $original_order_id),
($provider_settlement, 'REFUND', 5525000, 'DEBIT', $original_order_id);
Исходная запись дебета никогда не изменяется. Запись кредита возврата явно ссылается на исходную, создавая полный аудиторский след.
Почему это важно
Если злоумышленник скомпрометирует уровень приложения и попытается завысить баланс пользователя, записи реестра не будут сбалансированы. Регулярные проверки согласования обнаруживают это немедленно:
-- Запрос согласования: должен всегда возвращать 0
SELECT SUM(CASE direction
WHEN 'DEBIT' THEN amount_sun
WHEN 'CREDIT' THEN -amount_sun
END) as imbalance
FROM ledger;
Любой ненулевой результат запускает немедленное оповещение и расследование.
Принцип 3: атомарные операции баланса
Каждое изменение баланса использует SELECT FOR UPDATE для предотвращения состояний гонки. Это не опционально — это принудительно на уровне базы данных.
Проблема состояния гонки
Без надлежащей блокировки пользователь с балансом в 10 TRX мог бы отправить два одновременных заказа по 8 TRX каждый:
Поток 1: SELECT balance WHERE user_id = 1 -> 10 TRX
Поток 2: SELECT balance WHERE user_id = 1 -> 10 TRX
Поток 1: balance (10) >= order (8)? ДА -> продолжить
Поток 2: balance (10) >= order (8)? ДА -> продолжить
Поток 1: UPDATE balance = 10 - 8 = 2 TRX
Поток 2: UPDATE balance = 10 - 8 = 2 TRX
Результат: пользователь потратил 16 TRX с балансом только 10 TRX
Решение
BEGIN;
-- Блокируем строку - второй запрос ждёт здесь
SELECT balance_sun FROM accounts
WHERE user_id = $1
FOR UPDATE;
-- Проверяем баланс
-- Если недостаточно: ROLLBACK
-- Если достаточно: продолжаем
UPDATE accounts
SET balance_sun = balance_sun - $order_amount
WHERE user_id = $1
AND balance_sun >= $order_amount; -- Двойная проверка в UPDATE
COMMIT;
FOR UPDATE получает блокировку на уровне строки. Второй запрос блокируется до тех пор, пока первый не завершится или не откатится. После того как первый запрос завершится (сокращая баланс до 2 TRX), второй запрос прочитает обновлённый баланс (2 TRX) и правильно отклонит заказ с недостаточными средствами.
Принцип 4: валидация входных данных
Все входные данные проверяются с помощью схем Zod перед обработкой. Это включает запросы API, полезные нагрузки веб-хуков, ответы провайдеров и внутренние сообщения сервисов.
Валидация входных данных API
const CreateOrderSchema = z.object({
energy: z.number()
.int('Energy must be an integer')
.min(10000, 'Minimum order is 10,000 energy')
.max(100000000, 'Maximum order is 100,000,000 energy'),
targetAddress: z.string()
.regex(/^T[1-9A-HJ-NP-Za-km-z]{33}$/, 'Invalid TRON address format')
.refine(isValidTronAddress, 'Invalid TRON address checksum'),
duration: z.enum(['1h', '1d', '3d', '7d', '14d', '30d']),
maxPrice: z.number()
.positive()
.optional(),
idempotencyKey: z.string()
.max(255)
.optional()
});
Каждое поле типизировано, ограничено и проверено. Никакие необработанные данные пользователя не достигают бизнес-логики или запросов к базе данных.
Предотвращение SQL-инъекций
Все запросы к базе данных используют параметризованные операторы. Конкатенация строк никогда не используется для построения SQL:
// Никогда так:
const query = `SELECT * FROM users WHERE id = '${userId}'`; // SQL injection
// Всегда так:
const query = 'SELECT * FROM users WHERE id = $1';
const result = await db.query(query, [userId]);
Это принудительно выполняется проверкой кода и правилами linting. Нет механизма для интерполяции необработанных SQL-строк в кодовой базе.
Валидация TRON адреса
TRON адреса проверяются на нескольких уровнях:
- Проверка формата: должны соответствовать регулярному выражению TRON адреса (начинаются с T, 34 символа, base58).
- Проверка контрольной суммы: адрес включает контрольную сумму, которая обнаруживает опечатки.
- Проверка в блокчейне (опционально): подтвердить, что адрес существует и активирован.
Отправка energy на неверный адрес потратит ресурсы и не может быть отменена в блокчейне. Строгая валидация это предотвращает.
Принцип 5: изоляция сервисов
MERX работает как набор изолированных контейнеров Docker, каждый с минимальными разрешениями и без ненужного доступа.
Архитектура контейнеров
Docker network:
|
|-- api (port 3000, public-facing)
|-- price-monitor (no external ports)
|-- order-executor (no external ports)
|-- ledger (no external ports)
|-- treasury-signer (no external ports, Docker secret access)
|-- deposit-monitor (no external ports)
|-- withdrawal-executor (no external ports)
|
|-- postgresql (port 5432, internal only)
|-- redis (port 6379, internal only)
Ключевые свойства изоляции
- Сервис API не может получить доступ к приватному ключу казначейства. Только контейнер
treasury-signerможет прочитать секрет Docker, содержащий ключ. - Монитор цен не может изменять балансы. Он имеет только доступ на чтение к API провайдеров и доступ на запись к каналам Redis с ценами.
- Исполнитель заказов не может напрямую изменять реестр. Он публикует события расчёта в Redis, которые потребляет сервис реестра.
- PostgreSQL и Redis не раскрываются снаружи. Они доступны только из Docker сети.
Почему это важно
Если злоумышленник скомпрометирует сервис API (самый раскрытый компонент), он не может:
- Получить доступ к приватному ключу казначейства (другой контейнер, секрет Docker).
- Напрямую изменять записи реестра (другой сервис, без доступа на запись к таблицам реестра).
- Обойти проверки баланса (принудительно на уровне базы данных с FOR UPDATE).
Радиус взрыва при скомпрометировании любого одного сервиса ограничен по проектированию.
Принцип 6: ограничение частоты и предотвращение злоупотреблений
Ограничения частоты API
Каждая конечная точка API имеет ограничения частоты, подходящие для её назначения:
Публичные конечные точки (цены, здоровье): 100 запросов/минуту
Аутентифицированное чтение (заказы, баланс): 60 запросов/минуту
Аутентифицированная запись (создание заказа): 30 запросов/минуту
Выводы: 5 запросов/минуту
Ограничения частоты применяются к каждому API ключу, отслеживаются в Redis с использованием скользящих окон.
Защита при выводе
Выводы — это операция с наибольшим риском (перемещение реальных активов с платформы). Дополнительные защиты включают:
- Ограничение частоты: максимум 5 запросов вывода в минуту.
- Ограничения сумм: дневные лимиты вывода на счёт.
- Задержка подтверждения: большие выводы запускают период охлаждения.
- Проверка баланса:
SELECT FOR UPDATEобеспечивает достаточный баланс. - Идемпотентность: дублирующиеся запросы вывода (с тем же ключом идемпотентности) возвращают исходный результат.
Принцип 7: безопасность веб-хуков
MERX отправляет уведомления веб-хуков для обновлений статуса заказов, депозитов и других событий. Веб-хуки подписаны с помощью HMAC-SHA256 для предотвращения подделки.
Как работают веб-хуки HMAC
1. MERX вычисляет: HMAC-SHA256(webhook_body, your_webhook_secret)
2. MERX включает подпись в заголовок X-Merx-Signature
3. Ваш сервер пересчитывает HMAC с тем же секретом
4. Если подписи совпадают: подлинный веб-хук. Если нет: подделка, отбросить.
Проверка в коде
import crypto from 'crypto';
function verifyWebhook(body: string, signature: string, secret: string): boolean {
const computed = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computed)
);
}
Обратите внимание на использование timingSafeEqual для предотвращения атак по времени. Наивное сравнение строк (===) будет раскрывать информацию о правильной подписи через временные вариации ответа.
Принцип 8: управление секретами
Ни один секрет никогда не кодируется жёстко в исходном коде. Все чувствительные значения управляются через переменные окружения и секреты Docker.
Переменные окружения
# .env (никогда не коммитится в git)
DATABASE_URL=postgresql://...
REDIS_URL=redis://...
API_JWT_SECRET=...
WEBHOOK_SIGNING_SECRET=...
TRON_API_KEY=...
Секреты Docker (для очень чувствительных значений)
Приватный ключ казначейства слишком чувствителен для переменных окружения (которые могут быть залогированы или раскрыты через проверку процесса). Он хранится как секрет Docker:
# docker-compose.yml
services:
treasury-signer:
secrets:
- treasury_private_key
secrets:
treasury_private_key:
file: /run/secrets/treasury_key
Секреты Docker монтируются как файлы внутри контейнера, читаемые только процессом сервиса. Они не появляются в списках переменных окружения, выходе docker inspect или логах.
Защита Git
Файл .gitignore исключает все чувствительные файлы:
.env
*.key
*.pem
secrets/
Это устанавливается перед первым коммитом, а не после.
Мониторинг и реагирование на инциденты
Автоматизированные оповещения
Следующие условия запускают немедленные оповещения:
- Обнаружен дисбаланс реестра (несоответствие дебета и кредита).
- Баланс казначейства падает ниже порога.
- Неудачные попытки аутентификации превышают порог (10/минуту на IP).
- API провайдера возвращает неожиданные ошибки.
- Процент отказов выполнения заказа превышает 5%.
Логирование аудита
Каждая операция, относящаяся к безопасности, регистрируется со структурированными данными:
{
"event": "withdrawal_requested",
"user_id": "usr_abc123",
"amount_sun": 10000000,
"destination": "TAddress...",
"ip": "203.0.113.45",
"timestamp": "2026-03-30T12:00:00Z"
}
Логи хранятся для судебного анализа и соответствия требованиям. Они добавляются только и хранятся отдельно от данных приложения.
Заключение
Безопасность в MERX — это не одна функция, а набор взаимосвязанных принципов: отсутствие хранения ключей, двойная бухгалтерская книга, атомарные операции баланса, строгая валидация входных данных, изоляция сервисов, ограничение частоты, подписанные веб-хуки и надлежащее управление секретами. Каждый принцип решает конкретный вектор угрозы, и вместе они создают архитектуру защиты в глубину, где скомпрометирование любого одного компонента не скомпрометирует всю систему.
Ни одна система не неуязвима. Но, проектируя безопасность в архитектуру с самого начала — а не исправляя её позже — MERX минимизирует поверхность атаки и максимизирует затраты, которые должен понести злоумышленник, чтобы причинить вред.
Просмотрите компоненты с открытым исходным кодом: https://github.com/Hovsteder/merx-sdk-js, https://github.com/Hovsteder/merx-sdk-python, https://github.com/Hovsteder/merx-mcp.
Начните использовать MERX на https://merx.exchange.
Эта статья является частью технической серии MERX. MERX — первая блокчейн биржа ресурсов, построенная с безопасностью как фундаментальным требованием, а не как последующее расширение.
Попробуйте прямо сейчас с AI
Добавьте MERX в Claude Desktop или любой клиент, совместимый с MCP — без установки, API ключ не требуется для инструментов только для чтения:
{
"mcpServers": {
"merx": {
"url": "https://merx.exchange/mcp/sse"
}
}
}
Спросите вашего AI агента: "What is the cheapest TRON energy right now?" и получите живые цены от всех подключённых провайдеров.
Полная документация MCP: merx.exchange/docs/tools/mcp-server