gRPC API
Чтобы подключиться к Singula, не нужно ни регистрироваться, ни получать ключ. Хватит адреса Control Plane, файлов .proto и кошелька.
Правило доступа одно:
Всё, что касается сессии, денег или адреса узла, требует подписи: кошелька, анонимного доказательства или ключа самой сессии.
API-ключей в сети нет, и это сделано нарочно. Ключ кто-то должен выдать, хранить и уметь отозвать, и в итоге у него оказывается список всех, кто пользуется сетью. Подпись решает ту же задачу и ничего после себя не оставляет.
Полный перечень методов и полей — в справочнике API, он собирается из тех же .proto, что лежат на этом сайте. Эта глава рассказывает, как всем этим пользоваться и чего сеть ждёт от вашего клиента.
Четыре сервиса на одном порту
Заголовок раздела «Четыре сервиса на одном порту»Control Plane отдаёт четыре gRPC-сервиса на одном порту, по умолчанию 9090:
| Сервис | Для кого |
|---|---|
ClientService | клиентские приложения: сессии, чекпоинты, покупка Orbit |
AdminService | панели и мониторинг: здоровье, список звёзд |
NodeService | узлы |
FederationService | Control Plane между собой |
Вашему клиенту понадобятся первые два.
Адреса Control Plane нигде не зашиты. Каждый публикует хост и порт в CPEndpointRegistry, а адрес самого реестра клиент берёт из SingulaProtocol.snapshot():
getActiveEndpoints() returns (uint256[] starIds, string[] hosts, uint16[] ports, uint64[] updatedAts)Оператор может сменить порт в любой момент, так что читайте реестр при подключении, а не один раз при сборке. Порт 0 означает порт по умолчанию, 9090.
Что можно без подписи
Заголовок раздела «Что можно без подписи»Несколько методов нужны клиенту раньше, чем он что-нибудь подписал, поэтому они открыты всем:
ClientService.GetNetworkInfo— цены и адреса токенов, чтобы показать их на экране первой покупки.ClientService.ListNodes— узлы, которые этот Control Plane может выдать: идентификатор звезды, страна, загрузка и измеренная задержка. Пригодится, если клиент выбирает узел сам.ClientService.GetIssuerKey— ключ, которым Control Plane подписывает разрешения. Перед использованием клиент сверяет его сVoucherKeyRegistry.AdminService.GetHealth— жив ли Control Plane.AdminService.ListNodes— какие звёзды есть и живы ли они. Всё это и так видно в блокчейне.
Подпись на этих методах ничего бы не защитила, ведь кошелёк можно завести бесплатно и в любом количестве. А тем, кто ещё не вошёл, она помешала бы найти Control Plane.
Всё остальное требует одного из трёх доказательств:
- вход кошельком — для сессий, списка стран и транзакций без газа;
AccessProof— вместо кошелька, когда клиент открывает сессию или берёт разрешение анонимно;- ключ чекпоинтов сессии — для всего, что делается с уже открытой сессией.
TrafficCheckpoint,RenewSessionиSessionEnd, подписанные этим ключом, принимаются по любому соединению.
Адресов узлов без подписи не бывает. Открытый ListNodes называет звезду и говорит, жива ли она, но не отдаёт IP, порт и ключ REALITY. В блокчейне адресов узлов нет, и если бы Control Plane раздавал их всем, по ним можно было бы составить карту сети. Владелец звезды получает адреса своих узлов, приложив AdminAuth — подпись EIP-191, которая доказывает владение.
GetNetworkInfo — для удобства, а не для проверки. Адреса в нём сообщает тот самый Control Plane, с которым вы разговариваете. Всё, от чего зависят решения клиента, берите из SingulaProtocol через RPC, который выбрали сами.
Подключение и вход кошельком
Заголовок раздела «Подключение и вход кошельком»ClientService — это один двунаправленный поток:
rpc Connect(stream ClientMessage) returns (stream ControlPlaneMessage);Первым сообщением клиент отправляет ClientAuth с подписью вот такого текста:
Singula
Sign in to open a session on this Control Plane. The signature provesyou hold a subscription; it is not sent anywhere else.
This authorises no transaction and cannot move your funds.
Control Plane (Star id): <star id>Nonce: <nonce>Issued at (unix): <timestamp>Valid for: 7 daysЧто важно в подписи:
- Формат. EIP-191
personal_sign, текст байт в байт. Control Plane собирает его заново и восстанавливает из подписи адрес, так что лишний пробел или другой перевод строки ломают вход. - Получатель. В тексте стоит идентификатор звезды того Control Plane, к которому вы подключаетесь, и подпись с чужим идентификатором отклоняется. Так подпись годится только одному Control Plane, и он не сможет предъявить её от вашего имени другому.
- Срок. Подпись действует семь дней. Храните её и используйте при переподключениях, чтобы не спрашивать кошелёк каждый раз.
Текст написан обычными словами, потому что кошелёк показывает пользователю именно его. Человек должен понимать, что подписывает.
Если Control Plane ещё не знает свой идентификатор звезды, например сразу после запуска, он отказывает во входе.
Вход без кошелька
Заголовок раздела «Вход без кошелька»Анонимный клиент не отправляет ClientAuth. Вместо этого он кладёт прямо в SessionRequest два поля:
access—AccessProof, доказательство того, что он состоит в множестве подписчиков;permission— одно из разрешений Orbit на текущее окно.
Нужны оба поля. Запрос без разрешения отклоняется.
message AccessProof { uint64 epoch = 1; bytes merkle_root = 2; bytes nullifier = 3; uint32 index = 4; repeated bytes points = 5; // Groth16, 8 элементов}Как Control Plane проверяет доказательство:
- Эпоха. Её он не берёт из запроса, а пересобирает область доказательства из своего идентификатора и своей текущей эпохи. Поэтому доказательство действует у одного Control Plane и в одну эпоху.
- Слот.
index— номер слота квоты внутри эпохи. У разных слотов разные нуллификаторы, так что один Orbit может открыть за эпоху несколько сессий. - Корень Меркла. Подходит текущий или недавно сменённый. Корень двигается при любой регистрации в множестве, в том числе чужой, и без этой поблажки доказательство, построенное минуту назад, могло бы уже не пройти.
Как построить доказательство и взять разрешение, рассказано в главах анонимный доступ и построение доказательства.
Пока сессия жива
Заголовок раздела «Пока сессия жива»Клиент отправляет SessionRequest и получает SessionCredentials. Учётные данные шифрует сам узел, через ECDH на ваш session_public_key, так что Control Plane их передаёт, но прочитать не может.
Дальше у клиента три обязанности: отчитываться, продлевать сессию и закрыть её, когда она больше не нужна.
Отчитываться чекпоинтами
Заголовок раздела «Отчитываться чекпоинтами»Клиент должен регулярно отправлять TrafficCheckpoint: сколько байт прошло с начала сессии, текущее время и подпись ключом чекпоинтов. Это ключ Ed25519, открытую часть которого клиент назвал в checkpoint_public_key, когда открывал сессию.
Чекпоинты нужны для двух вещей:
- по ним видно, что сессия жива. Сессию, от которой около пяти минут не было чекпоинтов, Control Plane закрывает;
- по ним платят узлу. Из чекпоинтов складывается время, которое узел отработал.
Отправляйте чекпоинт примерно раз в 30 секунд. Чаще смысла нет, а реже вы рискуете тем, что сессию сочтут брошенной.
На каждый принятый чекпоинт Control Plane отвечает NonceUpdate, и в следующем чекпоинте должен стоять выданный там next_nonce. Так Control Plane отличает живую сессию от записанной и проигранной заново. Если связь оборвалась и nonce устарел, просто отправьте чекпоинт с последним известным: при верной подписи это не ошибка, Control Plane ответит свежим NonceUpdate.
Кому отправлять. Обычно тому же Control Plane, который выдал сессию. Но если в SessionCredentials ненулевой report_to_star_id, узел принадлежит другому оператору, и отчитываться нужно ему: только у него есть nonce и связь с узлом, а значит, только он может заплатить узлу. Найдите его адрес в CPEndpointRegistry и откройте туда отдельный поток для чекпоинтов этой сессии. Входить там не нужно — он узнаёт сессию по своей записи.
Если чекпоинт не принят, Control Plane объясняет почему:
| Что случилось | Ответ |
|---|---|
| сессия принадлежит другому Control Plane | ERROR_CODE_SESSION_NOT_FOUND, «this Control Plane cannot attribute that session» |
| у этого Control Plane нет nonce для сессии — скорее всего, вы отчитываетесь не туда | ERROR_CODE_SESSION_NOT_FOUND, «holds no nonce for that session» |
| nonce не совпал, и подпись тоже не от этой сессии | ERROR_CODE_AUTH_FAILED |
Что засчитывается. Платят за время, но от байтов зависит, засчитается ли оно:
- время копится, пока с последнего засчитанного чекпоинта не пройдёт 64 КиБ, и тогда засчитывается целиком, вместе с тихими минутами;
- если за 10 минут от последнего засчитанного чекпоинта 64 КиБ так и не набралось, накопленное сгорает, сколько бы чекпоинтов ни ушло за это время;
- чекпоинт, по которому выходит больше 10 Гбит/с, отклоняется.
Поэтому клиент должен отправлять настоящие счётчики. Все три числа — параметры сети, см. ограничения.
Продлевать сессию
Заголовок раздела «Продлевать сессию»SessionCredentials.expires_at — настоящий срок. Когда он наступает, узел удаляет учётную запись и туннель перестаёт работать. Чтобы сессия жила дальше, клиент должен заранее отправить RenewSessionRequest.
Запрос подписывается ключом чекпоинтов:
keccak256("singula-renew-v1" ‖ session_uuid ‖ 0x00 ‖ request_id ‖ timestamp)timestamp записывается восемью байтами big-endian и не должен расходиться с часами Control Plane больше чем на пять минут. Подпись доказывает, что запрос пришёл от владельца сессии, поэтому его можно прислать по любому соединению, хоть по короткому, открытому только ради продления. Отправляйте его тому Control Plane, которому сессия отчитывается.
Что ещё нужно предъявить:
| Сессия | Что добавить к запросу |
|---|---|
| кошельковая | ничего. Control Plane сам проверит, что Orbit ещё активен |
| анонимная, окно то же | ничего. Разрешение сессии остаётся за ней, пока она идёт |
| анонимная, началось новое окно | permission на новое окно |
В ответ приходит RenewSessionResult с новым expires_at. Начинайте продлевать с запасом и повторяйте, если не вышло: непродлённая сессия закончится в свой срок. Wanderer, например, начинает за десять минут и повторяет раз в минуту.
Закрыть сессию
Заголовок раздела «Закрыть сессию»Когда сессия больше не нужна, отправьте SessionEnd с итоговыми счётчиками. Тогда узел сразу освободит место, а анонимная сессия отдаст своё разрешение, и меньше чем через минуту его снова можно будет взять.
Кошельковую сессию закрывают из того же потока, где был вход. Анонимную — подписью ключа чекпоинтов по любому соединению:
keccak256("singula-end-v1" ‖ session_uuid ‖ final_bytes_uploaded ‖ final_bytes_downloaded ‖ timestamp)Счётчики и время — по восемь байт big-endian. На подписанный SessionEnd Control Plane отвечает SessionEndResult.
Если клиент пропал и ничего не прислал, сессию закроет Control Plane, когда перестанут приходить чекпоинты. Но разрешение до этого будет занято.
Газ за вас
Заголовок раздела «Газ за вас»Четыре операции Control Plane отправляет в блокчейн за свой счёт, по вашей подписи:
- покупка Orbit —
MintRequest; - его активация —
ActivationRequest; - публикация коммитмента для анонимного доступа —
RegisterCommitmentRequest; - в тестовой сети — пополнение кошелька тестовыми USDC, не больше чем до 100, —
FaucetRequest.
Перед отправкой Control Plane прогоняет транзакцию вхолостую и ту, что откатилась бы, не отправляет. Если ваш запрос отклонён с текстом отката, транзакции в блокчейне не было и газа ни с кого не списано.
С чего начать
Заголовок раздела «С чего начать»Скачайте client.proto и admin.proto, сгенерируйте клиент для своего языка и идите по главе собрать клиент. Wanderer построен на этом же API и ничем, кроме него, не пользуется.
