173 lines
33 KiB
Plaintext
173 lines
33 KiB
Plaintext
Сервер SHiNE Blockchain представляет собой бинарный WebSocket-сервис, обрабатывающий запросы клиентов в виде бинарных пакетов и возвращающий также бинарные ответы. Сервер управляет пользовательскими цепочками блоков (файлы формата .bch), проверяет подписи Ed25519 и хэши SHA-256, сохраняет данные на диск и позволяет получать информацию о состоянии цепочек и искать пользователей. Все цепочки и метаданные хранятся локально, без базы данных. Цепочки записываются в файлы data/{id}.bch, а сводная информация о пользователях хранится в JSON-файле data/blockchain_info.json.
|
||
|
||
Подключение клиента происходит по WebSocket на адрес ws://localhost:8080/ws или wss://shineup.me/ws. Протокол полностью бинарный, без текстовых или JSON-данных. Каждое сообщение клиента — отдельный бинарный пакет, сервер отвечает отдельным бинарным пакетом. Можно отправлять несколько запросов подряд, они обрабатываются независимо и асинхронно.
|
||
|
||
Любое сообщение клиента начинается с 4 байт, которые определяют тип операции (opCode). Данные кодируются в big-endian. После первых четырёх байт следует полезная нагрузка, структура которой зависит от конкретной операции. Ответ сервера также начинается с 4 байт — это код состояния (statusCode), после которого могут следовать дополнительные данные.
|
||
|
||
Основные операции протокола имеют следующие коды: 0 — PING, 1 — ADD_BLOCK, 2 — GET_BLOCKCHAIN, 30 — SEARCH_USERS. Код 0 используется для проверки соединения, сервер отвечает кодом 100 (PONG). Код 1 добавляет блок в цепочку. Код 2 возвращает полный бинарный файл блокчейна. Код 30 выполняет поиск пользователей по подстроке логина.
|
||
|
||
Коды ответов статусов: 100 (PONG) — ответ на пинг, 200 (OK) — операция успешна, 400 (BAD_REQUEST) — ошибка формата или данных, 404 (NOT_FOUND) — цепочка не найдена, 409 (ALREADY_EXISTS) — блок с таким номером уже существует, 412 (NON_SEQUENTIAL) — получен блок с номером больше ожидаемого, 422 (UNVERIFIED) — не совпал хэш или подпись, 500 (INTERNAL_ERROR) — исключение или внутренняя ошибка сервера. Все коды возвращаются в виде четырёх байт BigEndian.
|
||
|
||
Операция PING проста: клиент отправляет 4 байта со значением 0, сервер отвечает 4 байтами со значением 100. Это нужно для проверки активности соединения.
|
||
|
||
Главная операция — ADD_BLOCK (код 1). Она используется для добавления нового блока в существующую или новую цепочку. Формат запроса: первые 4 байта — число 1 (код операции), следующие 8 байт — идентификатор цепочки blockchainId в формате long BigEndian. После этого следует бинарный блок формата .bch, который полностью включает данные, подпись и хэш.
|
||
|
||
Формат файла .bch состоит из последовательности блоков без промежутков. Каждый блок имеет вид RAW + подпись (64 байта) + хэш (32 байта). RAW-часть состоит из заголовка размером 20 байт и тела произвольной длины. В заголовке содержатся поля: recordSize (4 байта, общий размер RAW), recordNumber (4 байта, порядковый номер блока), timestamp (8 байт, UNIX-время), recordType (2 байта, тип тела, 0=Header, 1=Text), recordTypeVersion (2 байта, версия структуры данного типа). После этого идёт само тело блока (body).
|
||
|
||
Тело блока определяется по типу. Тип 0 — HeaderBody, заголовок цепочки, создающий новую цепочку. Тип 1 — TextBody, простой текстовый блок. Для новых цепочек допускается только блок типа 0 с номером 0.
|
||
|
||
Когда сервер получает ADD_BLOCK, он сначала извлекает blockchainId и пытается найти информацию о цепочке в BchInfoManager. Если цепочки нет, то сервер допускает только блок-заголовок (type=0, num=0). Он парсится как HeaderBody, проверяется корректность логина, совпадение blockchainId и валидность подписи. Предыдущий хэш для первого блока считается нулевым (32 байта нулей). Если всё совпадает, создаётся новая запись о цепочке, создаётся файл data/{id}.bch, туда записывается бинарный блок, и в blockchain_info.json добавляется запись с логином, публичным ключом и текущим номером блока. Если в этом сценарии блок не является HeaderBody, или подпись невалидна, или blockchainId в теле не совпадает с заголовком — сервер возвращает код ошибки (обычно 400 или 422).
|
||
|
||
Если цепочка существует, сервер проверяет, что номер нового блока ровно на единицу больше последнего (lastBlockNumber + 1). Если номер меньше, возвращается 409 (блок уже есть), если больше — 412 (пропуск в номерах). После этого сервер берёт хэш последнего блока, собирает канонический preimage (логин UTF-8 + blockchainId + prevHash32 + rawBytes), вычисляет SHA-256, сравнивает с переданным hash32 и проверяет подпись Ed25519. Если оба совпадают, тело блока парсится (TextBody или другой тип), выполняется его логическая проверка (check). После успешной проверки блок дописывается в файл .bch, обновляется информация о состоянии цепочки (последний номер, новый хэш, общий размер). Если подпись или хэш не совпали, сервер возвращает 422.
|
||
|
||
Валидация подписи и хэша выполняется в классе BchCryptoVerifier. Preimage собирается из последовательности байт: сначала логин UTF-8 (без длины), затем 8 байт blockchainId BigEndian, затем 32 байта предыдущего хэша, затем сырые байты блока (RAW без подписи и хэша). Далее вычисляется SHA-256(preimage) и проверяется, что hash32 совпадает с ним. Подпись проверяется как Ed25519(preimage, publicKey32). Если хотя бы одна проверка не пройдена — блок отклоняется.
|
||
|
||
Операция GET_BLOCKCHAIN (код 2) используется для получения всего бинарного содержимого цепочки. Формат запроса: первые 4 байта — код 2, следующие 8 байт — blockchainId (BigEndian). Ответ сервера начинается с 4 байт (200 при успехе), затем 4 байта длины данных, затем идут байты содержимого файла целиком. Если цепочка не найдена, возвращается 404. Если произошла ошибка чтения — 500. Таким образом клиент может загрузить весь блокчейн-файл и при необходимости сам распарсить блоки.
|
||
|
||
Операция SEARCH_USERS (код 30) выполняет поиск логинов по подстроке. Формат запроса: первые 4 байта — код 30, следующие 4 байта — длина строки поиска N, затем N байт UTF-8 текста. Сервер выполняет поиск без учёта регистра по всем логинам, известным в blockchain_info.json, и возвращает максимум 5 совпадений. Ответ состоит из 4 байт (200 при успехе), затем 4 байт числа найденных пар, затем для каждой пары: 8 байт blockchainId BigEndian, 1 байт длины логина L, и L байт логина UTF-8 в оригинальном регистре. Если ничего не найдено, сервер возвращает 200 и число 0. Если запрос некорректен, возвращается 400.
|
||
|
||
Файловая система сервера устроена просто: в каталоге data хранятся все файлы. Каждый блокчейн имеет свой файл с именем {id}.bch. В этом файле последовательно записаны блоки в двоичном виде. Параллельно существует файл blockchain_info.json — JSON-объект, где ключ — blockchainId, а значение — структура с логином, публичным ключом в Base64, последним номером блока, хэшем последнего блока и размером цепочки. При добавлении нового блока сервер обновляет запись и сохраняет JSON на диск.
|
||
|
||
Сервер не требует аутентификации — безопасность обеспечивается криптографией: только владелец приватного ключа может подписывать блоки своей цепочки. Публичный ключ хранится в первом блоке HeaderBody и проверяется при добавлении.
|
||
|
||
Таким образом, чтобы написать клиент, нужно:
|
||
|
||
Уметь подключаться по WebSocket (ws или wss).
|
||
|
||
Формировать бинарные пакеты в формате BigEndian.
|
||
|
||
Для создания новой цепочки сгенерировать пару ключей Ed25519, собрать HeaderBody (с полями blockchainId, логин, публичный ключ), сериализовать его в байты, упаковать в блок типа 0 с номером 0, подписать preimage, добавить подпись и хэш, и отправить через ADD_BLOCK.
|
||
|
||
Для добавления обычного блока (тип 1) использовать тот же алгоритм: собрать preimage из логина, id, предыдущего хэша и RAW-байтов, подписать, прикрепить подпись и хэш, отправить.
|
||
|
||
Для чтения — вызвать GET_BLOCKCHAIN и распарсить ответ.
|
||
|
||
Для поиска пользователей — отправить SEARCH_USERS и распарсить пары.
|
||
|
||
Для проверки соединения — PING.
|
||
|
||
Все числовые значения всегда в BigEndian. Все строки в UTF-8. Все подписи — Ed25519 длиной 64 байта. Все хэши — SHA-256 длиной 32 байта.
|
||
|
||
Типы тел блоков:
|
||
HeaderBody (тип 0) содержит фиксированные поля: ASCII тег "SHiNE", blockchainId (8 байт), длину логина, логин UTF-8, четыре зарезервированных числа (type, number, version, prevId), и 32 байта публичного ключа.
|
||
TextBody (тип 1) содержит просто UTF-8-строку без дополнительных полей.
|
||
|
||
Клиент, который реализует этот протокол, должен уметь: открывать WebSocket-соединение, отправлять бинарные пакеты по описанным форматам, вычислять SHA-256 и подписи Ed25519, и интерпретировать ответы сервера по первым четырём байтам статуса. Вся логика взаимодействия основана на простых числовых кодах и фиксированных структурах без сложных заголовков.
|
||
|
||
Главное правило — сервер никогда не принимает блок, если не совпадает подпись или хэш. Поэтому клиент обязан правильно формировать preimage и использовать правильный публичный/приватный ключ. Также сервер строго следит за порядком номеров блоков. Валидация тела блока проверяет только базовую корректность (непустой логин, корректный UTF-8). Все остальные проверки лежат на клиенте.
|
||
|
||
В результате взаимодействия через этот протокол клиент может создавать собственные цепочки блоков, добавлять в них данные, загружать свои или чужие цепочки и искать пользователей по логину. Формат прост, двоичный и полностью детерминирован.
|
||
|
||
|
||
Формат блока .bch является центральным элементом протокола. Каждый блок хранится последовательно, без промежутков. Цепочка — это просто последовательность таких блоков, записанных подряд в бинарный файл. Каждый блок состоит из трёх частей: RAW-часть (все данные без подписи и хэша), подпись длиной 64 байта и хэш длиной 32 байта. Общая длина блока равна длине RAW + 96 байт. Сервер при чтении файла просто двигается от начала к концу, разбирая один блок за другим, опираясь на поле recordSize, которое всегда указывает длину RAW-части (тело без подписи и хэша).
|
||
|
||
RAW-часть имеет фиксированный формат и начинается с 20 байт заголовка. Эти 20 байт — это общая «шапка» для любого типа блока. В ней хранятся:
|
||
|
||
recordSize — 4 байта, целое число BigEndian, равное длине всей RAW-части (20 + длина тела).
|
||
|
||
recordNumber — 4 байта, номер блока в цепочке, начиная с нуля.
|
||
|
||
timestamp — 8 байт, время создания блока в секундах Unix Time.
|
||
|
||
recordType — 2 байта, короткое число, тип тела (0 = HeaderBody, 1 = TextBody, в будущем могут быть другие).
|
||
|
||
recordTypeVersion — 2 байта, версия структуры тела (для HeaderBody всегда 1, для TextBody тоже 1).
|
||
После этих 20 байт сразу идёт тело блока (body), которое имеет разный формат в зависимости от типа.
|
||
|
||
Тип 0 — HeaderBody, заголовочный блок, всегда имеет номер 0 и используется для создания новой цепочки. Его структура тела строго определена и содержит:
|
||
• 8 байт ASCII-строки “SHiNE” — это сигнатура формата.
|
||
• 8 байт long BigEndian — blockchainId, уникальный идентификатор цепочки.
|
||
• 1 байт — длина логина пользователя N.
|
||
• N байт — логин пользователя в UTF-8 (без завершающего нуля).
|
||
• 4 байта int BigEndian — blockchainType (пока всегда 0).
|
||
• 4 байта int BigEndian — blockchainNumber (пока всегда 0).
|
||
• 2 байта short BigEndian — версия формата пользователя (всегда 1).
|
||
• 8 байт long BigEndian — prevUserBchId (всегда 0).
|
||
• 32 байта — публичный ключ пользователя Ed25519 (publicKey32).
|
||
|
||
После этих данных никакого дополнительного контента нет. Суммарная длина тела зависит от длины логина. Таким образом, для HeaderBody: длина RAW = 20 + (8 + 8 + 1 + N + 4 + 4 + 2 + 8 + 32). Этот блок полностью описывает владельца цепочки и его публичный ключ. При создании нового блокчейна сервер разрешает только один такой блок с номером 0 и типом 0.
|
||
|
||
Тип 1 — TextBody, обычный текстовый блок. Его тело состоит только из текста UTF-8, без дополнительных метаданных. Это могут быть сообщения, комментарии, данные или команды. Любые последующие блоки (номер 1, 2, 3 и т.д.) обычно имеют тип 1. При сериализации тело просто представляет собой байты строки в кодировке UTF-8.
|
||
|
||
После RAW-части добавляются две секции: подпись (signature64) и хэш (hash32). Подпись — это 64 байта, результат Ed25519.sign(preimage, privateKey). Хэш — это 32 байта, результат SHA-256(preimage).
|
||
|
||
Ключевое понятие — preimage. Это каноническая бинарная последовательность, из которой формируется хэш и подпись. Она состоит из следующих частей, строго в указанном порядке:
|
||
|
||
байты логина пользователя в UTF-8 (без длины, просто сами байты);
|
||
|
||
8 байт идентификатора цепочки blockchainId в формате long BigEndian;
|
||
|
||
32 байта предыдущего хэша prevHash32 (для самого первого блока — это 32 нуля);
|
||
|
||
байты RAW-части (включая заголовок и тело, но без подписи и хэша).
|
||
|
||
Из этого preimage берутся два результата:
|
||
• hash32 = SHA-256(preimage)
|
||
• signature64 = Ed25519.sign(preimage, privateKey32)
|
||
|
||
Хэш хранится в блоке в конце и используется для проверки целостности при добавлении следующего блока. Следующий блок при вычислении своего preimage уже использует этот хэш как prevHash32. Таким образом, создаётся цепочка зависимых хэшей, образующих полную криптографическую связанность всех блоков. Подпись подтверждает, что именно владелец приватного ключа подписал блок. Сервер проверяет это через Ed25519.verify(preimage, signature64, publicKey32).
|
||
|
||
Итоговая структура полного блока (FULL) выглядит так:
|
||
• 4 байта recordSize
|
||
• 4 байта recordNumber
|
||
• 8 байт timestamp
|
||
• 2 байта recordType
|
||
• 2 байта recordTypeVersion
|
||
• M байт body
|
||
• 64 байта signature64
|
||
• 32 байта hash32
|
||
|
||
Общая длина блока равна recordSize + 96. При чтении из файла сервер сначала берёт recordSize, по нему знает, где заканчивается тело, а после этого считывает ещё 64 байта подписи и 32 байта хэша.
|
||
|
||
Важно понимать: сервер не принимает блок, если хотя бы одно из условий нарушено — несоответствие длины, некорректный UTF-8 в теле, неправильная подпись, неверный хэш или сбитая последовательность номеров. Сервер также не принимает новый блок, если его номер не совпадает с ожидаемым (т.е. если цепочка уже имеет последний номер N, следующий блок должен иметь номер N+1).
|
||
|
||
При генерации нового блока клиент должен выполнить последовательность шагов:
|
||
|
||
Собрать тело блока (body). Для HeaderBody — с нужным логином и публичным ключом. Для TextBody — просто текст.
|
||
|
||
Сформировать RAW-часть: заполнить 20 байт заголовка, указать длину тела, номер блока, время, тип и версию, затем добавить тело.
|
||
|
||
Собрать preimage (логин UTF-8 + blockchainId + prevHash32 + rawBytes). Для первого блока prevHash32 — 32 нуля.
|
||
|
||
Посчитать SHA-256(preimage) и сохранить как hash32.
|
||
|
||
Подписать preimage своим приватным ключом Ed25519 и получить signature64.
|
||
|
||
Объединить rawBytes + signature64 + hash32 в один массив.
|
||
|
||
Отправить этот массив серверу в запросе ADD_BLOCK (после поля blockchainId).
|
||
|
||
Если всё сделано правильно, сервер проверит подпись и хэш, убедится, что цепочка корректна, и добавит блок. В случае успеха сервер вернёт 200. Если подпись или хэш неверные — 422. Если номер блока не совпадает с ожидаемым — 409 или 412.
|
||
|
||
Таким образом, клиент может последовательно создавать цепочку блоков, где каждый следующий блок зависит от предыдущего через хэш. Формат абсолютно детерминирован и не допускает вариаций. Все целые числа записываются в формате BigEndian. Все строки кодируются в UTF-8 без завершающего нуля. Все подписи Ed25519 имеют длину 64 байта, все хэши SHA-256 имеют длину 32 байта. Цепочка считается валидной, если каждый блок проходит проверку по своей подписи и хэшу, а номера блоков непрерывны от 0 и далее.
|
||
|
||
Эта структура едина как для клиентской стороны, так и для сервера. Клиент, следуя этому формату, способен полностью создавать, подписывать и проверять блоки офлайн, а затем синхронизировать их с сервером, просто отправляя бинарные блоки в ADD_BLOCK. Сервер при получении блока повторяет ту же логику вычисления preimage и сверяет подпись и хэш, гарантируя, что ни один байт блока не был изменён.
|
||
|
||
Таким образом, формат блока .bch является криптографически связанной последовательностью структур, каждая из которых включает заголовок, тело, подпись и хэш, и все они формируют надёжную цепочку данных, проверяемую без участия центральной базы данных.
|
||
|
||
|
||
|
||
|
||
Рекомендации для клиента и описание операции GET_LAST_BLOCK_INFO.
|
||
|
||
Клиент хранит у себя приватный ключ пользователя, который никогда не передаётся серверу. Из этого приватного ключа вычисляется публичный ключ Ed25519, который используется для подписи блоков и проверки на сервере. Публичный ключ передаётся только один раз — внутри первого блока HeaderBody, создающего новую цепочку. После этого сервер хранит у себя только публичный ключ и логин пользователя, и проверяет подписи всех следующих блоков именно по нему. Приватный ключ остаётся исключительно у клиента.
|
||
|
||
Клиент может создавать приватный ключ двумя способами. Первый — случайная генерация 32 байт (seed) через криптографический генератор случайных чисел. Второй, более удобный — детерминированная генерация из пароля пользователя: берётся строка пароля UTF-8, вычисляется SHA-256 от неё, и результат (32 байта) используется как приватный ключ. Это позволяет восстановить тот же ключ из одного и того же пароля без хранения seed в явном виде. Из приватного ключа вычисляется публичный через Ed25519. Таким образом, клиент может всегда получить ту же пару (private/public), просто имея пароль.
|
||
|
||
На стороне клиента рекомендуется хранить три параметра для каждой цепочки: приватный ключ (или пароль, из которого он создаётся), публичный ключ и идентификатор цепочки blockchainId. Идентификатор цепочки — это уникальное 8-байтное число (long), которое выбирается клиентом при создании новой цепочки. Он может быть сгенерирован случайно, взят из системного счётчика или рассчитан как часть хэша от логина, но сервер не навязывает конкретный способ — важно только, чтобы значение было уникально.
|
||
|
||
Для синхронизации с сервером клиент должен отслеживать состояние последнего блока: его номер и хэш. Эти значения необходимы при формировании следующего блока, чтобы корректно подставить prevHash32 в preimage. Сервер предоставляет отдельную операцию для получения этой информации.
|
||
|
||
Операция GET_LAST_BLOCK_INFO имеет код 31 и используется для запроса состояния выбранной цепочки. Клиент отправляет запрос, состоящий из 12 байт: первые 4 байта — код операции (int 31 в BigEndian), следующие 8 байт — идентификатор цепочки blockchainId (long BigEndian). Сервер проверяет наличие цепочки и возвращает 40 байт данных.
|
||
|
||
Ответ от сервера имеет следующую структуру:
|
||
• 4 байта — код статуса (200 при успехе, 404 если цепочка не найдена, 500 при ошибке);
|
||
• 4 байта — номер последнего блока (int BigEndian, если цепочка пуста — 0);
|
||
• 32 байта — хэш последнего блока SHA-256 (если цепочка пуста, все нули).
|
||
|
||
Таким образом, клиент может в любой момент узнать, до какого блока сервер синхронизирован, чтобы не отправлять повторно уже существующие данные. Если сервер вернул 404, это значит, что цепочка с указанным blockchainId ещё не существует, и клиент может начать новую, отправив HeaderBody через ADD_BLOCK. Если сервер вернул 200, клиент использует полученный номер и хэш для формирования следующего блока: новый recordNumber должен быть на единицу больше полученного, а prevHash32 при вычислении preimage — это хэш, возвращённый сервером.
|
||
|
||
Рекомендуется, чтобы клиент при каждом запуске сначала вызывал GET_LAST_BLOCK_INFO для своей цепочки, сверял локальное состояние с сервером, и только после этого создавал или отправлял новые блоки. Это обеспечивает целостность цепочки и правильную последовательность номеров.
|
||
|
||
Таким образом, клиент хранит у себя всё необходимое для подписи и создания блоков — приватный ключ, логин, blockchainId и последнее состояние. Сервер же хранит только публичный ключ, логин, текущий номер блока и последний хэш. Все вычисления подписи и хэша выполняются клиентом локально. Это гарантирует, что сервер не может подделать или изменить ни один блок, а клиент при необходимости может полностью восстановить всю цепочку, просто имея свой пароль или приватный ключ.
|