SHA256
GPT: сгенерено и не проверено (смена ключей пользователя)
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
# 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 не переключён.
|
||||
Reference in New Issue
Block a user