Построение доказательства
Это практическая часть анонимного уровня. Что значит доказательство и что с ним делает 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) | indexcpStarId— Control Plane, к которому вы идёте;epoch— текущая эпоха по часам блокчейна;index— слот квоты, с нуля.
Все три должны быть в области, а не в сообщении. Semaphore выводит нуллификатор только из (secret, scope), поэтому слот, положенный в сообщение, даёт всем слотам один нуллификатор.
Сообщением передайте сам index.
Ведите учёт потраченных слотов сами. Control Plane не может сказать, какие из ваших слотов заняты: отличать одного подписчика от другого — ровно то, чего он делать не должен. Держите счётчик на каждый Control Plane и эпоху и переходите к следующему слоту, когда текущий вернулся как использованный. Слоты обнуляются каждую эпоху.
Область разрешения
Заголовок раздела «Область разрешения»Каждое разрешение запрашивается своим доказательством, и строится оно не по области выше.
scope = (2 << 128) | (window << 64) | index2— доменная метка, которая отделяет доказательства для разрешений от доказательств для сессий;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, 1 | A.x, A.y |
| 2, 3 | B.x.c1, B.x.c0 — наоборот |
| 4, 5 | B.y.c1, B.y.c0 — наоборот |
| 6, 7 | C.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 сессии, а не кошельком. Остальное — в главе собрать клиент.
Если отклонили
Заголовок раздела «Если отклонили»Отказ называет причину. Эти пять идут от самого доказательства; полный список, с разрешениями, — в разделе если что-то не работает.
- Доказательство не проходит проверку — проверьте, что
circuitVersion()совпадает с вашими артефактами, что у дерева глубина 16 и что порядок точек — как в таблице выше. - Множество не узнано — ваш корень устарел. Пересоберите его: корень двигает любая регистрация или удаление, в том числе чужие, а Control Plane принимает текущий корень и один недавно сменённый.
- Право уже использовано — нуллификатор этого слота потрачен. Возьмите следующий слот.
- Квота эпохи исчерпана — все слоты этой эпохи у этого Control Plane потрачены. У другого Control Plane своя квота.
- Слот израсходовал бюджет — узловое время слота потрачено. Решение то же, что выше.
