SHA256
321 lines
17 KiB
Markdown
321 lines
17 KiB
Markdown
# 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 не переключён.
|