# Key Rotation API Этот раздел описывает публичные JSON/WebSocket операции мастера смены ключей пользователя. Публично доступны: - `KeyRotationStart` - `KeyRotationStatus` - `KeyRotationAddBlock` - `KeyRotationFinishChain` - `KeyRotationRotatePda` - `KeyRotationContinue` - `KeyRotationAbort` После `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 ```json { "op": "KeyRotationStart", "requestId": "kr-1", "payload": { "newRootKey": "", "newBlockchainKey": "", "newClientKey": "", "forkFromBlock": 120, "forkFromHash": "<64 hex>", "reasonCode": 1, "comment": "Плановая смена пароля" } } ``` `reasonCode`: 1. обычная ротация; 2. возможная компрометация; 3. подтверждённая компрометация / rollback; 4. 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 ```json { "op": "KeyRotationStatus", "requestId": "kr-status-1", "payload": {} } ``` Если активной ротации нет: ```json { "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 ```json { "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 ```json { "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 ```json { "op": "KeyRotationRotatePda", "requestId": "kr-rotate-pda-1", "payload": { "pdaRotationSignature": "" } } ``` Операция разрешена только после `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-блоки повторно проходят обычный `AddBlock` validation/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: ```json { "op": "KeyRotationContinue", "requestId": "kr-continue-1", "payload": {} } ``` `KeyRotationContinue` не переводит деньги и не перешифровывает DM. Это специально оставленные точки расширения для будущей реализации. ## `KeyRotationAbort` Прерывает ротацию только пока Solana-ротация PDA ещё не могла быть отправлена: - разрешено из `COPYING_CHAIN`; - разрешено из `CHAIN_READY`; - начиная с `ROTATING_PDA` отмена запрещена, процесс можно только довести вперёд. Request: ```json { "op": "KeyRotationAbort", "requestId": "kr-abort-1", "payload": {} } ``` При `ABORTED` временные candidate-строки удаляются из PostgreSQL. Уже опубликованные Arweave DataItem остаются неизменяемым сиротским историческим следом и не становятся активной цепочкой, поскольку PDA не переключён.