Перейти к основному содержимому

Протокол релея Epicbox

Epicbox — это релей с хранением и пересылкой, который переносит слейты (slate) между кошельками, не имеющими прямой связи друг с другом, что позволяет выполнять переводы между двумя кошельками за NAT.

Версия протокола: 3.0.0.

Что делает релей

Он принимает подписанные блобы слейтов, адресованные публичному ключу, хранит их и доставляет, когда получатель подключается и подтверждает владение этим ключом.

Он никогда не получает ключевой материал, а его единственная криптографическая операция — проверка подписей.

Релей видит публичные ключи, между которыми маршрутизирует данные, время их взаимодействия и IP-адреса, с которых они подключаются. Суммы находятся внутри обязательств (commitment) слейта и ему не видны.

Транспорт и адресация

WebSocket поверх TLS, порт 443 по умолчанию. Сообщения — JSON-объекты с полем type.

Адреса имеют вид <public_key>@<domain>[:<port>], где public_key — 52 символа base58-check, кодирующие сжатый публичный ключ secp256k1. См. addresses.

Ключ формируется для каждого аккаунта, поэтому кошелёк хранит один epicbox-адрес для каждого своего аккаунта, а слушатель подписывается с адресом открытого аккаунта (impls/src/adapters/epicbox.rs:145). См. accounts.

Домен релея кошелька по умолчанию — epicbox.epiccash.com.

Аутентификация

Владение адресом подтверждается подписью вызова. Приватный ключ никогда не покидает кошелёк.

  1. При подключении, а затем с повторяющимся интервалом, релей отправляет Challenge со случайной строкой.
  2. Клиент отвечает Subscribe, указывая свой адрес и подпись над этим вызовом.
  3. Релей проверяет подпись. Корректная подпись подтверждает владение, и клиент может получить очередь для этого адреса.

Интервал вызова составляет 60 секунд по умолчанию — это определяет максимальное время ожидания поставленного в очередь слейта до того, как он будет предложен подключённому клиенту. Клиент, уже подключённый и зарегистрированный как слушатель, получает слейты немедленно, не дожидаясь следующего вызова.

Клиентам, объявляющим версию протокола 2.0.0 или не объявляющим её вовсе, выдаётся фиксированная строка вызова вместо случайной — для совместимости с кошельками старше версии 3.5.2.

Типы сообщений

От клиента к релею

ТипПоляНазначение
Subscribeaddress, ver, signatureПодтвердить владение и начать получение
UnsubscribeaddressОстановить получение и закрыть
PostSlatefrom, to, str, signatureОтправить слейт (slate) для доставки
Madeaddress, signature, ver, epicboxmsgidПодтвердить обработку слейта
ClientDetailswallet_version, wallet_mode, protocol_versionОбъявить идентификатор клиента
ping / pongnoneKeepalive

Challenge, GetVersion и FastSend принимаются для обеспечения совместимости и не являются частью 3.0.0.

Клиент, объявляющий wallet_mode равным listener, регистрируется для немедленной доставки, поэтому слейты, поступающие во время его подключения, передаются напрямую.

От релея к клиенту

ТипПоляНазначение
ChallengestrПодписать для подписки
OknoneПринято
Slatefrom, str, signature, challenge, ver, epicboxmsgidСлейт для вас
Errorkind, descriptionОтклонено

Виды ошибок: UnknownError, InvalidRequest, InvalidSignature, InvalidChallenge, TooManySubscriptions.

Публикация слейта

PostSlate проходит двойную проверку перед сохранением: формат адреса from и to, а также подпись над полезной нагрузкой относительно ключа в адресе from.

Слейт, чей домен и порт назначения совпадают с собственной конфигурацией релея, сохраняется локально. Любое другое назначение пересылается на соответствующий релей через новое исходящее WebSocket-соединение — именно так работает доставка между разными релеями.

Доставка и её ограничения

Доставка подтверждается, а не выполняется по принципу «отправил и забыл» — это основное изменение по сравнению с протоколом 2.0.0.

  1. При корректном Subscribe релей отправляет самый старый недоставленный слейт для этого ключа вместе с epicboxmsgid.
  2. Клиент обрабатывает его и отвечает Made, подписывая epicboxmsgid.
  3. Слейт помечается как доставленный и следующий освобождается только при корректном Made.

Очередь для каждого получателя, таким образом, последовательна: одновременно обрабатывается только один слейт. Клиент, прекративший подтверждать получение, блокирует собственную очередь.

Применяются два ограничения, и меньшее из них — не срок хранения:

Срок хранения, 7 дней. Слейты хранятся 604,800 секунд, после чего удаляются независимо от состояния.

Лимит подтверждений, примерно девять раундов проверки. Релей считает попытки отправки на каждый сокет. После трёх неподтверждённых попыток он сбрасывает счётчик и начинает новый раунд, а после трёх таких раундов — девяти попыток суммарно — отбрасывает все недоставленные слейты (slate) для данного получателя, а не только тот, который не доставлялся (app_mongo.js:366). При интервале проверки 60 секунд это составляет около девяти минут.

An accepted post is not a delivered slate

Ни один из лимитов не уведомляет отправителя. Со стороны отправителя слейт был принят и затем перестал существовать, тогда как его входы остаются зарезервированными до отмены перевода. См. что освобождает их.

Дополнительные характеристики доставки:

Состояние доставки привязано к соединению. Счётчики подтверждений живут вместе с сокетом, поэтому переподключение сбрасывает их, а отдельные экземпляры релея за балансировщиком нагрузки не разделяют их. В референсном развёртывании клиент закрепляется за одним экземпляром по IP.

Перенаправленные слейты не хранятся локально. Слейт, предназначенный для другого релея, перенаправляется через новое исходящее соединение без сохранения на релее, который его принял.

Поведение клиента

Клиент релея в кошельке:

  • Переподключается после задержки в 5 секунд и продолжает повторные попытки.
  • После публикации финализированной транзакции опрашивает мемпул узла раз в секунду в течение до four минут.
  • Использует slate V2, поэтому ни доказательство платежа, ни ttl_cutoff_height не передаются. См. выбор транспорта.

Запустить релей

Референсное развёртывание запускает два экземпляра релея за nginx, с MongoDB для хранения и вспомогательным бинарником на Rust для проверки подписей и адресов.

git clone https://github.com/EpicCash/epic-epicbox-docker.git
cd epic-epicbox-docker
git submodule update --init --recursive
EPICBOX_DOMAIN=epicbox.example.com docker compose up -d --build

Направить кошельки на него:

[epicbox]
epicbox_domain = "epicbox.example.com"
epicbox_port = 443

Конфигурация берётся из переменных окружения или default_config.json, при этом переменные окружения имеют приоритет. Значимые ключи: EPICBOX_DOMAIN, EPICBOX_PORT, MONGO_URL, CHALLENGE_INTERVAL (миллисекунды, значение по умолчанию 60000), DEBUG, STATS.

Требования для production:

Индексы MongoDB создаются из mongo-init.js. Compose-файл монтирует его в /docker-entrypoint-initdb.d, поэтому свежий том MongoDB получает индексы очереди и TTL-индекс на createdat автоматически. Если вы направляете релей на существующую MongoDB, создайте их вручную; те же команды находятся в блоке комментариев в начале app_mongo.js. Без TTL-индекса слейты никогда не истекают.

Установите STATS в true. По умолчанию оно отключено. Почасовые счётчики, которые оно предоставляет, хранятся в памяти и сбрасываются при перезапуске, поэтому собирайте их, если нужна история.

Референсный compose-файл публикует два экземпляра релея на портах хоста 8888 и 8889, а nginx на 8443. Сопоставьте их с 443 в вашем развёртывании или отредактируйте compose-файл.

Исходный код

Далее