Files
SHiNE-server/docs/API/19_Key_Rotation_API.md
T

17 KiB
Raw Blame History

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

{
  "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:

  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

{
  "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-блоки повторно проходят обычный 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:

{
  "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 не переключён.