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

Построение доказательства

Это практическая часть анонимного уровня. Что значит доказательство и что с ним делает Control Plane — в главе анонимный доступ.

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

Semaphore v4, глубина дерева 16.

Версия опубликована в блокчейне и должна совпадать символ в символ:

SubscriberRegistry.circuitVersion() → "semaphore/4.14.3/16"

Адрес реестра берётся из SingulaProtocol.snapshot(). Прочитайте версию при запуске и сравните со своими артефактами.

Нужны генератор свидетеля .wasm и ключ доказательства .zkey, оба для глубины дерева 16 и оба под ту версию, которую называет реестр. Берите официальные артефакты Semaphore v4 из опубликованного проектом доверенного сетапа — те же файлы, что Wanderer поставляет как semaphore-16.wasm и semaphore-16.zkey. Свои не генерируйте: ключ из другой церемонии описывает другую схему, и все доказательства, построенные с ним, отклонят.

Кладите их внутрь приложения, а не скачивайте. Причин две, и вторая важнее.

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

А .wasm вычисляет свидетеля из вашего секрета. Тот, кто отдаёт вам этот файл, запускает код над значением, из которого выведена вся ваша личность. С .zkey ставки ниже: подменённый ключ доказательства просто даёт доказательства, которые не проходят, потому что правильность решает ключ проверки на другой стороне.

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

Singula Anonymous Identity v1

Подпись не используется напрямую: секретом личности становится её keccak256:

secret = keccak256(utf8(signature))

Так он приводится к 32 байтам в нужном диапазоне. Это не защита: у того, кто знает подпись, до того же секрета один keccak256.

Относитесь к подписи как к самой личности. Утёкшая подпись может забрать ваши разрешения и открывать сессии за ваш счёт.

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

Дерево строится из всех коммитментов реестра по порядку:

SubscriberRegistry.totalCommitments()
SubscriberRegistry.commitmentsSlice(start, count)

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

Сохраняйте нули. Ячейка удалённого участника обнуляется, а не выбрасывается, поэтому commitmentsSlice возвращает нули на месте ушедших. Отфильтруете их — сдвинутся все следующие позиции, и получится корень, которого больше никто не считает.

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

Три значения в одном числе:

scope = (cpStarId << 96) | (epoch << 32) | index
  • cpStarId — Control Plane, к которому вы идёте;
  • epoch — текущая эпоха по часам блокчейна;
  • index — слот квоты, с нуля.

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

Сообщением передайте сам index.

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

Каждое разрешение запрашивается своим доказательством, и строится оно не по области выше.

scope = (2 << 128) | (window << 64) | index
  • 2 — доменная метка, которая отделяет доказательства для разрешений от доказательств для сессий;
  • window — окно, на которое берётся разрешение; это эпоха;
  • index — номер разрешения, от нуля до опубликованного издателем числа минус один.

Идентификатора Control Plane здесь нет, и в этом весь смысл. Нуллификатор выводится из области, поэтому область, не называющая оператора, даёт один и тот же нуллификатор везде — и именно это делает каждый номер единственным на Orbit за окно во всей сети.

Это же значение передайте и как сообщение. Проверяющий восстановит оба из своего окна и index в вашем TakePacketRequest.

Доменная метка важна: без неё доказательство для получения разрешения можно было бы переиграть в открытие сессии.

Окно никогда не берётся из вашего запроса — издатель смотрит на свои часы.

Схема видит сообщение и область как хеши, а не как числа:

h(v) = keccak256(be32(v)) >> 8

Значение в 32 байтах big-endian, keccak256, потом сдвиг вправо на 8 бит, чтобы результат попал в скалярное поле. Официальная библиотека Semaphore делает это сама.

Здесь сторонние клиенты ломаются чаще всего.

Доказательство передаётся восемью элементами поля в порядке calldata Ethereum, а не в том, что выдаёт snarkjs: у B два коэффициента каждой пары Fq2 переставлены.

ИндексСодержимое
0, 1A.x, A.y
2, 3B.x.c1, B.x.c0 — наоборот
4, 5B.y.c1, B.y.c0 — наоборот
6, 7C.x, C.y

Каждый элемент — ровно 32 байта big-endian.

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

message AccessProof {
uint64 epoch = 1;
bytes merkle_root = 2;
bytes nullifier = 3;
uint32 index = 4;
repeated bytes points = 5; // 8 элементов по 32 байта
}

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

Для сессии доказательство кладётся в SessionRequest. Для разрешения — в TakePacketRequest рядом с его index: то же сообщение, другая область.

Дальше чекпоинты подписываются одноразовым ключом ed25519 сессии, а не кошельком. Остальное — в главе собрать клиент.

Отказ называет причину. Эти пять идут от самого доказательства; полный список, с разрешениями, — в разделе если что-то не работает.

  1. Доказательство не проходит проверку — проверьте, что circuitVersion() совпадает с вашими артефактами, что у дерева глубина 16 и что порядок точек — как в таблице выше.
  2. Множество не узнано — ваш корень устарел. Пересоберите его: корень двигает любая регистрация или удаление, в том числе чужие, а Control Plane принимает текущий корень и один недавно сменённый.
  3. Право уже использовано — нуллификатор этого слота потрачен. Возьмите следующий слот.
  4. Квота эпохи исчерпана — все слоты этой эпохи у этого Control Plane потрачены. У другого Control Plane своя квота.
  5. Слот израсходовал бюджет — узловое время слота потрачено. Решение то же, что выше.