17 KiB
Key Rotation API
Этот раздел описывает публичные JSON/WebSocket операции мастера смены ключей пользователя.
Публично доступны:
KeyRotationStartKeyRotationStatusKeyRotationAddBlockKeyRotationFinishChainKeyRotationRotatePdaKeyRotationContinueKeyRotationAbort
После PDA_ROTATED отдельный фоновый worker автоматически переводит ротацию в REBUILDING_SERVER, делает новую candidate-chain единственной рабочей цепочкой PostgreSQL и затем переводит процесс в WALLET_MIGRATION.
Общие правила
- операции доступны только авторизованному LOCAL-пользователю своего access server;
- login берётся из авторизованной сессии и не передаётся в payload;
- приватные ключи и пароли серверу не передаются никогда;
- в БД сохраняются только старые/новые публичные ключи;
- первая серверная запись появляется сразу в состоянии
COPYING_CHAIN; состоянияPREPARINGв БД нет; - во время
KeyRotationStartсервер захватывает тот же per-blockchain lock, что использует обычныйAddBlock, и фиксирует непротиворечивый source tip.
KeyRotationStart
Создаёт новую серверную сессию ротации.
Request
{
"op": "KeyRotationStart",
"requestId": "kr-1",
"payload": {
"newRootKey": "<Base58 или Base64 публичного ключа 32B>",
"newBlockchainKey": "<Base58 или Base64 публичного ключа 32B>",
"newClientKey": "<Base58 или Base64 публичного ключа 32B>",
"forkFromBlock": 120,
"forkFromHash": "<64 hex>",
"reasonCode": 1,
"comment": "Плановая смена пароля"
}
}
reasonCode:
- обычная ротация;
- возможная компрометация;
- подтверждённая компрометация / rollback;
- recovery.
Сервер самостоятельно берёт из текущего PDA:
oldRootKey;oldBlockchainKey;oldClientKey;- текущий
sourceBlockchainName.
Также сервер самостоятельно фиксирует текущий tip цепочки. Клиент не может подменить эти значения в запросе.
forkFromBlock/forkFromHash обязаны указывать на реально существующий блок текущей активной цепочки.
Новые root/blockchain/client keys должны быть валидными 32-байтовыми публичными ключами, отличаться от соответствующих старых ключей и друг от друга.
После успеха создаётся key_rotation_sessions со статусом COPYING_CHAIN. progressTotal равен количеству будущих candidate-блоков: копия 0..forkFromBlock плюс TECH_FORK.
Success response
Ответ использует тот же payload состояния, что и KeyRotationStatus.
KeyRotationStatus
Возвращает текущее состояние ротации авторизованного пользователя.
Request
{
"op": "KeyRotationStatus",
"requestId": "kr-status-1",
"payload": {}
}
Если активной ротации нет:
{
"op": "KeyRotationStatus",
"requestId": "kr-status-1",
"status": 200,
"ok": true,
"payload": {
"rotationStatus": "NONE"
}
}
Если ротация есть, payload содержит:
rotationSessionId;rotationStatus;sourceBlockchainName/candidateBlockchainName;- старые и новые публичные root/blockchain/client keys;
forkFromBlock/forkFromHash;sourceTipBlock/sourceTipHash;reasonCode/comment;progressCurrent/progressTotal;pdaRotationSignature(когда появится на последующем этапе);walletMigrationStatus;messageMigrationStatus;lastError/retryCount;createdAtMs/updatedAtMs.
Любая авторизованная сессия пользователя может читать этот статус. Состояние ротации принадлежит аккаунту, а не конкретному WebSocket-сеансу.
KeyRotationAddBlock
Принимает один ANS-104 DataItem будущего fork на этапе COPYING_CHAIN. Candidate-блоки хранятся отдельно от обычной таблицы blocks, поэтому до переключения fork они не создают лайки, ответы, каналы, связи и другие materialized effects.
Request
{
"op": "KeyRotationAddBlock",
"requestId": "kr-block-1",
"payload": {
"blockNumber": 0,
"prevBlockHash": "",
"blockBytesB64": "<полный ANS-104 DataItem в Base64>"
}
}
Правила:
- операция доступна только при
rotationStatus=COPYING_CHAIN; - блоки принимаются строго последовательно:
0..forkFromBlock, затем одинTECH_FORKс номеромforkFromBlock+1; - каждый DataItem обязан быть подписан
newBlockchainKey; - для копируемых блоков Frame, ANS-104 tags, target и anchor должны полностью совпадать с исходным блоком; меняются только owner/signature/DataItem id;
- последний блок обязан быть
TECH_FORK, а его parent key, fork point, старый tip, reason/comment и число отброшенных блоков должны совпадать сKeyRotationStart; - повтор уже принятого идентичного блока безопасен и возвращает success; другой DataItem/hash на том же номере возвращает conflict;
- candidate-блоки попадают в отдельную очередь Arweave/Turbo publisher-а и имеют приоритет перед обычными блоками;
progressCurrentувеличивается только после фактической успешной публикации DataItem в Arweave/Turbo. ПоэтомуKeyRotationStatusпоказывает реальный прогресс публикации, а не только приём сервером.
Основные ошибки
KEY_ROTATION_NOT_COPYING— ротация не находится вCOPYING_CHAIN;KEY_ROTATION_BLOCK_OUT_OF_ORDER— пропущен предыдущий candidate-блок;KEY_ROTATION_BLOCK_CONFLICT— на этом номере уже сохранён другой candidate-блок;KEY_ROTATION_BAD_SIGNATURE— DataItem подписан не новым blockchain key;KEY_ROTATION_FRAME_MISMATCH— копируемый Frame отличается от исходной цепочки;KEY_ROTATION_TAGS_MISMATCH— изменены ANS-104 tags копируемого блока;KEY_ROTATION_TECH_FORK_REQUIRED— вместо финальногоTECH_FORKпередан другой блок;- ошибки
KEY_ROTATION_TECH_FORK_*— поляTECH_FORKне соответствуют зафиксированной rotation session.
Основные ошибки KeyRotationStart
AUTH_REQUIRED— нет авторизованной сессии;KEY_ROTATION_ALREADY_ACTIVE— для login уже выполняется ротация;KEY_ROTATION_BAD_FIELDS— некорректные public keys/hash;KEY_ROTATION_BAD_NEW_KEYS— ключи не изменились либо новые ключи совпадают друг с другом;KEY_ROTATION_BAD_REASON— reason вне диапазона1..4;KEY_ROTATION_COMMENT_TOO_LONG— комментарий больше 1024 UTF-8 байт;KEY_ROTATION_BAD_FORK_POINT— неверная точка fork;KEY_ROTATION_FORK_BLOCK_NOT_FOUND— выбранный блок отсутствует;KEY_ROTATION_FORK_HASH_MISMATCH— переданный hash не совпадает с сервером;KEY_ROTATION_SESSION_STALE— авторизованная сессия содержит уже неактуальный blockchainName.
KeyRotationFinishChain
Финализирует этап построения candidate-chain. Операция не меняет PDA и не переключает активную цепочку пользователя: она только доказывает, что будущий fork уже полностью сохранён и опубликован в Arweave/Turbo.
Request
{
"op": "KeyRotationFinishChain",
"requestId": "kr-finish-chain-1",
"payload": {}
}
Перед переходом в CHAIN_READY сервер повторно проверяет:
- количество candidate-блоков равно
progressTotal(0..forkFromBlock+ одинTECH_FORK); - каждый candidate-блок уже подтверждён Arweave/Turbo publisher-ом;
- номера идут строго
0,1,2,...без дырок; blockHashиDataItem idсовпадают с сохранёнными значениями;- каждый DataItem подписан
newBlockchainKey; block 0имеет нулевойprevHash, а каждый следующий блок ссылается на hash предыдущего Frame;- копируемые блоки всё ещё байт-в-байт совпадают с выбранным префиксом исходной цепочки по Frame, tags, target и anchor;
- последний блок является корректным
TECH_FORKи повторяет зафиксированные fork point, parent tip, reason/comment и число отброшенных блоков.
Только после успешной финальной проверки выполняется:
COPYING_CHAIN -> CHAIN_READY.
Повторный KeyRotationFinishChain, когда ротация уже находится в CHAIN_READY, идемпотентно возвращает success. Это позволяет безопасно вызывать операцию из нескольких сессий или повторить её после потери ответа.
Основные ошибки
KEY_ROTATION_NOT_ACTIVE— активная ротация отсутствует;KEY_ROTATION_NOT_COPYING— текущий этап уже не позволяет завершать candidate-chain;KEY_ROTATION_CHAIN_INCOMPLETE— сервер получил не все candidate-блоки;KEY_ROTATION_CHAIN_NOT_PUBLISHED— не все DataItem подтверждены Arweave/Turbo publisher-ом;KEY_ROTATION_CANDIDATE_GAP/KEY_ROTATION_CHAIN_HASH_MISMATCH— нарушена последовательность candidate-chain;KEY_ROTATION_BAD_SIGNATURE— сохранённый candidate не подтверждается новым blockchain key;KEY_ROTATION_TECH_FORK_*— финальныйTECH_FORKбольше не соответствует rotation session;KEY_ROTATION_FINISH_CHAIN_RACE— состояние ротации было одновременно изменено другой операцией.
KeyRotationRotatePda
Фиксирует, что клиент уже локально подписал и отправил в Solana транзакцию полной ротации PDA.
Сервер не получает приватные ключи и не подписывает транзакцию. Клиент передаёт только публичную Solana transaction signature.
Request
{
"op": "KeyRotationRotatePda",
"requestId": "kr-rotate-pda-1",
"payload": {
"pdaRotationSignature": "<Base58 Solana transaction signature>"
}
}
Операция разрешена только после CHAIN_READY. Сервер атомарно сохраняет signature и переводит:
CHAIN_READY -> ROTATING_PDA.
После этого отмена ротации уже запрещена: отправленная Solana-транзакция может подтвердиться позже, даже если клиент потерял соединение.
Подтверждением успешной ротации служит не сам факт наличия signature, а фактический current PDA, увиденный Solana sync. Sync требует одновременного совпадения:
rootKey == newRootKey;blockchainKey == newBlockchainKey;clientKey == newClientKey;blockchainName == candidateBlockchainName.
Только после этого сервер атомарно переводит:
ROTATING_PDA -> PDA_ROTATED.
Если Solana sync успел увидеть новое PDA раньше вызова KeyRotationRotatePda, он умеет подтвердить ожидаемые ключи прямо из CHAIN_READY. Поэтому потеря клиентского запроса после уже подтверждённой Solana-транзакции не оставляет ротацию зависшей. Если API-вызов всё же приходит, handler идемпотентно возвращает текущее подтверждённое состояние.
Повтор с той же signature идемпотентен. Другая signature для уже начатого ROTATING_PDA возвращает conflict.
Основные ошибки
KEY_ROTATION_NOT_ACTIVE— активной ротации нет;KEY_ROTATION_NOT_CHAIN_READY— candidate-chain ещё не подтверждена;KEY_ROTATION_BAD_SOLANA_SIGNATURE— signature отсутствует или не Base58;KEY_ROTATION_PDA_SIGNATURE_CONFLICT— для этой ротации уже сохранена другая signature;KEY_ROTATION_ROTATE_PDA_FAILED— внутренняя ошибка фиксации этапа.
Автоматический rebuild после PDA_ROTATED
После подтверждения нового current PDA сервер сам выполняет:
PDA_ROTATED -> REBUILDING_SERVER -> WALLET_MIGRATION.
Во время rebuild:
- проверяется, что current PDA действительно указывает на
candidateBlockchainName/newBlockchainKey; - старая активная цепочка удаляется из рабочих PostgreSQL-таблиц;
- candidate-блоки повторно проходят обычный
AddBlockvalidation/projection path; - runtime-cache
to_bch_nameу логических ссылокlogin + blockNumber + blockHashперепривязывается к новому fork; - входящие
likes_count/replies_countпересчитываются; - после
COMPLETEвременные строки candidate-chain удаляются из PostgreSQL.
Если rebuild прерывается, REBUILDING_SERVER остаётся активным, ошибка записывается в lastError/retryCount, а worker безопасно повторяет rebuild.
KeyRotationContinue
Продолжает интерактивные post-PDA этапы. В текущей версии два будущих этапа являются честными заглушками:
WALLET_MIGRATION:walletMigrationStatus = NOT_IMPLEMENTED, затем переход вMESSAGE_MIGRATION;MESSAGE_MIGRATION:messageMigrationStatus = NOT_IMPLEMENTED, затемFINALIZING -> COMPLETE.
Request:
{
"op": "KeyRotationContinue",
"requestId": "kr-continue-1",
"payload": {}
}
KeyRotationContinue не переводит деньги и не перешифровывает DM. Это специально оставленные точки расширения для будущей реализации.
KeyRotationAbort
Прерывает ротацию только пока Solana-ротация PDA ещё не могла быть отправлена:
- разрешено из
COPYING_CHAIN; - разрешено из
CHAIN_READY; - начиная с
ROTATING_PDAотмена запрещена, процесс можно только довести вперёд.
Request:
{
"op": "KeyRotationAbort",
"requestId": "kr-abort-1",
"payload": {}
}
При ABORTED временные candidate-строки удаляются из PostgreSQL. Уже опубликованные Arweave DataItem остаются неизменяемым сиротским историческим следом и не становятся активной цепочкой, поскольку PDA не переключён.