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

321 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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": "<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
```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": "<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:
```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 не переключён.