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

Анонимный доступ

Право на сессию клиент может показать двумя способами. С кошельком он подписывает сообщение, и Control Plane знает, чья это сессия. Анонимно Control Plane узнаёт только одно: подключился кто-то из множества подписчиков.

Эта глава объясняет, как устроен анонимный путь и что для него нужно от клиента. Само доказательство по шагам строится в главе построение доказательства.

У подписчика один секрет, а из него — по одной личности на каждый слот Orbit. Их коммитменты — хеши — лежат в SubscriberRegistry рядом со всеми остальными.

Секрет получается из подписи кошелька над постоянной строкой:

Singula Anonymous Identity v1

Строка никогда не меняется. Изменишь один символ — получишь другую личность, а для пользователя это выглядит как потеря Orbit.

Личность слота 0 выводится из подписи, личность слота i — из секрета слота 0:

secret_0 = "0x" ‖ hex(keccak256(utf8(подпись)))
secret_i = "0x" ‖ hex(keccak256(utf8(secret_0) ‖ utf8("/slot/" ‖ i)))

Так одна подпись даёт все личности на любом устройстве, а продлённый Orbit регистрирует те же самые.

Вывод детерминированный, поэтому личность переезжает вместе с кошельком: тот же кошелёк на новой машине подпишет ту же строку и получит тот же секрет. Ничего не нужно экспортировать и сохранять.

Это держится на том, что подпись кошелька детерминирована. RFC 6979 так устроен, и кошельки на практике так себя ведут, но ни один стандарт этого не обещает. Поэтому клиенту стоит сверить полученный коммитмент с зарегистрированным в блокчейне, прежде чем на него полагаться: кошелёк со случайным nonce молча выдаст другую личность.

Секрет не покидает машину. Ходят только доказательства, построенные из него.

Анонимность здесь — это число людей: вы прячетесь среди всех, кто зарегистрировал коммитмент. Прежде чем на это полагаться, посмотрите текущий размер через SubscriberRegistry.totalCommitments().

По одному коммитменту на слот: Orbit на три слота регистрирует три, и каждый годится для одной сессии за раз. Что они принадлежат одному Orbit, публично, как и вся регистрация; какой из них использован в сессии — нет. Поэтому размер множества честнее считать в Orbit, а не в коммитментах.

Регистрация порождает событие Registered. Control Plane отправляет её через GasStation, и газ уже оплачен при покупке: достаточно вашей подписи, ETH не нужен.

Лучше всего регистрироваться при активации, а не тогда, когда анонимная сессия понадобилась впервые. SubscriptionNFT позволяет назвать коммитменты в самой активации и регистрирует их в той же транзакции: кошелёк подписывает один раз, множество раньше получает участника, а анонимный путь готов к первому же обращению. Wanderer всегда активирует именно так.

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

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

Область упаковывает три значения:

scope = (cpStarId << 96) | (epoch << 32) | index

Все три лежат в области, а не делятся между областью и сообщением. Semaphore выводит нуллификатор из (secret, scope), сообщение в этом не участвует, поэтому слот, положенный в сообщение, дал бы всем слотам один и тот же нуллификатор.

По порядку, от дешёвого к дорогому:

  1. Слот. index должен быть меньше квоты эпохи.
  2. Множество. Корень должен быть знакомым: текущим или только что сменённым. Доказательство по незнакомому множеству вообще нельзя оценить.
  3. Право. Нуллификатор не должен быть использован в этой эпохе.
  4. Доказательство. Проверяется по области, которую Control Plane собирает сам из своего идентификатора звезды, своей текущей эпохи и заявленного слота. Эпоха из запроса не используется.
  5. Граница эпохи. Если проверка не прошла в первые пять минут эпохи, пробуется предыдущая — ровно одна. Доказательство, построенное за секунды до смены, — не жульничество.
  6. Бюджет. Слот не должен был израсходовать своё узловое время.
  7. Списание — в самом конце. Нуллификатор сжигается только после всех проверок, так что неверное доказательство слот не тратит.

Отказ называет, какая из проверок не прошла. Точные тексты — в разделе если что-то не работает.

Слот — это index внутри эпохи, и у каждого слота свой нуллификатор. По умолчанию у Orbit 25 открытий за эпоху у каждого Control Plane — счёт отдельный, потому что идентификатор звезды входит в область, — и у каждого слота бюджет в 2 часа узлового времени. Оба числа — параметры сети, одинаковые везде; см. ограничения.

Считать одни открытия было бы неправильно: пятьдесят коротких сессий стоят сети гораздо меньше, чем одна на весь день. Бюджет — вторая половина, и вместе они задают, сколько один Orbit может потратить у одного оператора за эпоху.

Control Plane не может сказать клиенту, какие из его слотов уже потрачены: отличать одного подписчика от другого — ровно то, чего он делать не должен. Поэтому клиент ведёт счёт сам, для каждого Control Plane и эпохи, и переходит к следующему слоту, когда текущий оказался занят.

Квота считается у каждого оператора отдельно, поэтому Orbit, исчерпавший её у одного Control Plane, просто начинает заново у следующего. Ограничивают одновременные сессии во всей сети разрешения.

Разрешение даёт право держать одну сессию за раз. Пока сессия идёт, разрешение числится занятым в множестве, общем для всех Control Plane. Оно подписано вслепую: издатель подписывает значение, которого не видит, поэтому не узнает разрешение потом — ни у себя, ни у других. Общее множество показывает, сколько разрешений занято, но не чьи.

У Orbit есть фиксированное число разрешений на окно, где окно — это эпоха. Они пронумерованы с 0. Число задаёт издатель и фиксирует в блокчейне вместе со своим ключом; сейчас это 5.

Каждый номер берётся отдельно и только один раз за окно во всей сети: доказательство для номера даёт один и тот же нуллификатор у любого Control Plane, а Control Plane сообщают друг другу, какие номера выдали. Устройства с одним Orbit берут разные номера. Любой Control Plane может выдать ещё не взятый номер, и разрешение работает у любого оператора, кто бы его ни подписал.

Берите разрешения заранее, у Control Plane, который только что проверили на живость, а не в момент, когда сессия уже нужна. Получение выглядит так:

1. Запросить ключ издателя. Отправьте GetIssuerKeyRequest. В ответ придут DER-ключ, идентификатор звезды издателя, окно и число разрешений.

2. Сверить ключ с блокчейном.

VoucherKeyRegistry.getEpochKey(issuerStarId, epoch)
returns (bytes32 keyHash, uint32 denominationGB, uint64 setAt, bool issuerActive)

Требуйте issuerActive, требуйте, чтобы denominationGB совпадал с числом, которое вам назвали, и чтобы keyHash равнялся keccak256(DER ‖ denominationGB), где число записано четырьмя байтами big-endian. Хеш посчитает computeKeyHash.

Читайте реестр по адресу из SingulaProtocol, а не по тому, который назвал Control Plane: сверка ключа с коммитментом, выбранным той же стороной, ничего не доказывает.

Ключ публикуется на целую эпоху вперёд и никогда не может быть записан на текущую. Именно это позволяет клиенту проверить, что ему дали «тот же ключ, что и всем».

3. Ослепить и попросить. Сгенерируйте случайный секрет в 32 байта, ослепите его под ключом издателя по RFC 9474, вариант RSABSSA-SHA384-PSS-Randomized, и отправьте в TakePacketRequest с нужным index и доказательством по области разрешения для этого номера. Один ослеплённый секрет на запрос.

Если в ответе стоит index_taken, номер в этом окне уже занят — другим устройством или раньше этим же. Попросите другой. Перебирайте номера в случайном порядке, тогда два устройства одного Orbit редко потянутся к одному и тому же.

4. Раскрыть. Ответ раскрывается в подпись над подготовленным сообщением — randomizer ‖ secret, 64 байта, — потому что рандомизированный вариант подписывает подготовленную форму, а не секрет. Храните пару вместе с номером и окном: клиент, сохранивший только секрет, останется с подписью, которую не сможет предъявить.

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

Положите Permission в SessionRequest рядом с AccessProof. Нужны оба: доказательство говорит, что вы подписчик прямо сейчас, а разрешение — сколько сессий Orbit может держать одновременно. Запрос с доказательством, но без разрешения отклоняется.

Отправьте вместе с ним открытый ключ издателя. Control Plane не верит ему на слово, а сверяет с коммитментом в блокчейне, так что передать ключ — значит сэкономить обращение, ничего не потеряв.

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

Поэтому клиенту стоит закрывать сессии, когда они больше не нужны: отправить SessionEnd, подписанный ключом чекпоинтов сессии (см. закрыть сессию). Клиент, который просто пропал, держит разрешение занятым, пока Control Plane не сочтёт сессию брошенной.

Продление внутри окна сохраняет то же разрешение. Для продления в следующее окно нужно разрешение на это окно.

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

Раз в области есть идентификатор Control Plane, у разных Control Plane один и тот же подписчик даёт разные нуллификаторы. Два оператора, сравнив списки, не найдут общего, а в блокчейн нуллификаторы не попадают вовсе.

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

Тот же дайджест называется и в блокчейне: каждый интервал, который Control Plane отправляет в EpochAttestations, несёт разрешение, на котором он шёл. Сколько разрешение было занято — публично, чьё оно — скрыто.

И для доказательства принадлежности ничего не выдаётся: клиент строит его из секрета, который никуда не уходит, поэтому ни один Control Plane не присутствует при появлении права. На практике холодный кошелёк подписывает один раз, когда выводится секрет, и никогда — на каждую сессию.

  • Первый узел видит ваш IP. Анонимный доступ скрывает, кто вы, от Control Plane, но не то, откуда вы подключаетесь к узлу.
  • Множество может быть маленьким. Доказательство от этого не слабее, а вот толпа, в которой вы прячетесь, меньше. Смотрите totalCommitments().
  • Регистрация публична. Событие Registered называет токен Orbit, а через него — кошелёк, который его держит. Wanderer регистрирует каждый Orbit при активации, поэтому событие говорит, что у кошелька есть Orbit; какие сессии его, остаётся скрытым.
  • Два уровня в одной сессии не смешиваются. Подпись называет вас, доказательство — нет.
  • Время не скрыто. В множестве из двух человек, кто когда подключился, говорит само за себя.

Что видит каждый участник, разобрано в главе кто что видит.