SHA256
304 lines
11 KiB
Markdown
304 lines
11 KiB
Markdown
# Импорт доверенных SHINE-ARCHIVE и Blockchain Viewer
|
||
|
||
Этот документ описывает вторую половину архивной системы: как обычный SHiNE-сервер узнаёт о новых archive head других серверов, кому доверяет, как импортирует недостающие SHiNE-блоки и как UI получает ссылку на историю конкретной `blockchain_name`.
|
||
|
||
## 1. Источник archive head
|
||
|
||
Archive importer **не делает отдельные Solana RPC-запросы**.
|
||
|
||
Уже существующий Solana Users Sync разбирает User PDA block type `100` и копирует его в локальную таблицу:
|
||
|
||
```text
|
||
solana_user_pda_current.archive_head_tx_id
|
||
solana_user_pda_current.archive_head_hash
|
||
```
|
||
|
||
Для локального состояния importer v23 добавляет туда же:
|
||
|
||
```text
|
||
archive_imported BOOLEAN
|
||
archive_last_imported_tx_id TEXT
|
||
```
|
||
|
||
Это локальные поля сервера, в Solana они не записываются.
|
||
|
||
Когда обычный Solana Users Sync видит тот же archive head повторно, `archive_imported` сохраняется как есть.
|
||
|
||
Когда `archive_head_tx_id` или `archive_head_hash` изменился:
|
||
|
||
```text
|
||
archive_imported = false
|
||
```
|
||
|
||
а `archive_last_imported_tx_id` сохраняет последнюю успешно обработанную точку и позволяет продолжить после сбоя.
|
||
|
||
## 2. Whitelist доверенных publisher-ов
|
||
|
||
В `application.properties` задаётся список логинов серверов, архивы которых разрешено принимать:
|
||
|
||
```properties
|
||
archive.import.allowedPublishers=archive-server-1,archive-server-2
|
||
archive.import.intervalMinutes=60
|
||
archive.import.workDir=data/archive-import
|
||
```
|
||
|
||
Правила:
|
||
|
||
- логины разделяются запятыми;
|
||
- сравнение без учёта регистра;
|
||
- пустое `archive.import.allowedPublishers=` полностью выключает importer;
|
||
- принимаются только строки `solana_user_pda_current` с `is_server=true`;
|
||
- whitelist является только первым фильтром, криптографические проверки всё равно обязательны.
|
||
|
||
## 3. Периодическая проверка
|
||
|
||
После запуска сервера importer делает первую проверку примерно через 10 секунд, затем по умолчанию раз в 60 минут.
|
||
|
||
Каждый цикл — дешёвый запрос только к локальной PostgreSQL:
|
||
|
||
```text
|
||
approved publisher
|
||
AND is_server=true
|
||
AND archive_head_tx_id != ''
|
||
AND archive_imported=false
|
||
```
|
||
|
||
Если таких строк нет, Arweave не вызывается.
|
||
|
||
## 4. Проверки archive block
|
||
|
||
Для каждого pending publisher сервер проверяет:
|
||
|
||
1. publisher находится в whitelist;
|
||
2. `archive_head_tx_id/archive_head_hash` уже пришли через обычный User PDA sync;
|
||
3. SHA-256 скачанного файла совпадает с `archive_head_hash`;
|
||
4. `creator_login == closer_login == publisher login`;
|
||
5. Ed25519 archive signature проверяется root key publisher-а из User PDA;
|
||
6. каждый вложенный raw SHiNE block проходит обычную SHiNE-проверку через существующий `AddBlock` path.
|
||
|
||
Подпись большого архива защищает контейнер и навигацию, а подписи обычных SHiNE blocks защищают сами пользовательские данные.
|
||
|
||
## 5. Как определяется, что head новый
|
||
|
||
Отдельная таблица обработанных TX для основной логики не нужна.
|
||
|
||
Текущий User PDA snapshot уже содержит:
|
||
|
||
```text
|
||
archive_head_tx_id
|
||
archive_head_hash
|
||
archive_imported
|
||
archive_last_imported_tx_id
|
||
```
|
||
|
||
Пример:
|
||
|
||
```text
|
||
archive_head_tx_id = TX100
|
||
archive_imported = false
|
||
archive_last_imported_tx_id = TX97
|
||
```
|
||
|
||
Это означает: Solana уже объявила `TX100` текущей головой publisher-а, но локальный сервер успел импортировать только до `TX97`.
|
||
|
||
После полной успешной обработки `TX100`:
|
||
|
||
```text
|
||
archive_imported = true
|
||
archive_last_imported_tx_id = TX100
|
||
```
|
||
|
||
При следующем новом PDA head Users Sync сам сбросит `archive_imported=false`.
|
||
|
||
## 6. Догон пропущенных больших блоков
|
||
|
||
Если сервер был выключен и вместо `TX97` сразу увидел `TX100`, он скачивает и проверяет `TX100`, читает его FULL reference table и находит `TX97`.
|
||
|
||
После этого импортирует только:
|
||
|
||
```text
|
||
TX98
|
||
TX99
|
||
TX100
|
||
```
|
||
|
||
После каждого полностью импортированного большого блока `archive_last_imported_tx_id` сдвигается вперёд.
|
||
|
||
Если сервер впервые видит publisher и `archive_last_imported_tx_id` пустой, импортируются все previous refs от старых к новым, затем текущий head.
|
||
|
||
Если непустой `archive_last_imported_tx_id` отсутствует в FULL history текущего head, importer останавливается: это рассматривается как возможная смена/fork archive chain, а не как повод молча забыть старый cursor.
|
||
|
||
## 7. Crash recovery
|
||
|
||
Если процесс упал после `TX98`, но до `TX100`:
|
||
|
||
```text
|
||
archive_imported = false
|
||
archive_last_imported_tx_id = TX98
|
||
```
|
||
|
||
Следующий часовой цикл продолжит с `TX99`.
|
||
|
||
Если процесс успел импортировать head и записать `archive_last_imported_tx_id = TX100`, но упал до установки `archive_imported=true`, следующий цикл просто завершит отметку без повторной загрузки всей цепочки.
|
||
|
||
Все cursor updates выполняются условно по ожидаемому `archive_head_tx_id`. Если обычный Solana Users Sync успел заменить head во время импорта, старый процесс не сможет пометить новый head импортированным.
|
||
|
||
## 8. Импорт `UserBlockchainChunk`
|
||
|
||
Для каждого chunk:
|
||
|
||
1. берётся lock этой `blockchain_name`;
|
||
2. если локального `blockchain_state` нет, identity создаётся по синхронизированному User PDA;
|
||
3. raw records разбираются как обычные `BchBlockEntry`;
|
||
4. уже существующий block допускается только при совпадении hash;
|
||
5. новый block должен идти строго `localLast + 1`;
|
||
6. новый block добавляется существующим validator/write path;
|
||
7. конфликт hash или gap останавливает импорт этой archive chain.
|
||
|
||
## 9. Индекс последнего archive chunk
|
||
|
||
Таблица:
|
||
|
||
```text
|
||
archive_blockchain_location
|
||
```
|
||
|
||
содержит для каждой `blockchain_name`:
|
||
|
||
```text
|
||
blockchain_name
|
||
publisher_login
|
||
arweave_tx_id
|
||
archive_hash
|
||
big_block_number
|
||
chunk_offset
|
||
chunk_size
|
||
source_last_block_number
|
||
updated_at_ms
|
||
```
|
||
|
||
Если blockchain встретилась в новом archive block, её location обновляется. Если не встретилась — старая ссылка остаётся.
|
||
|
||
Эту таблицу заполняют как trusted importer, так и локальный archive publisher.
|
||
|
||
## 10. API для UI
|
||
|
||
WS operation:
|
||
|
||
```text
|
||
GetArchiveBlockchainLocation
|
||
```
|
||
|
||
Request:
|
||
|
||
```json
|
||
{
|
||
"op": "GetArchiveBlockchainLocation",
|
||
"blockchainName": "alice-001"
|
||
}
|
||
```
|
||
|
||
Response содержит:
|
||
|
||
```text
|
||
blockchainName
|
||
publisherLogin
|
||
arweaveTxId
|
||
archiveHash
|
||
bigBlockNumber
|
||
chunkOffset
|
||
chunkSize
|
||
sourceLastBlockNumber
|
||
```
|
||
|
||
## 11. UI и ссылка Viewer
|
||
|
||
В настройках пользователя есть экран `Архив блокчейна`.
|
||
|
||
Viewer-файл:
|
||
|
||
```text
|
||
shine-UI/Blockchain-Viewer.html
|
||
```
|
||
|
||
Основные параметры ссылки:
|
||
|
||
```text
|
||
/Blockchain-Viewer.html
|
||
?tx=<ARWEAVE_TX_ID>
|
||
&offset=<CHUNK_OFFSET>
|
||
&size=<CHUNK_SIZE>
|
||
&blockchain=<BLOCKCHAIN_NAME>
|
||
```
|
||
|
||
Дополнительно:
|
||
|
||
```text
|
||
&channel=<CHANNEL_NAME>
|
||
&message=<BLOCK_NUMBER>
|
||
```
|
||
|
||
`blockchain` используется также для проверки: если загруженный chunk имеет другое имя blockchain, Viewer прекращает обработку.
|
||
|
||
`channel` открывает нужный канал, а `message` прокручивает к указанному сообщению/block number и выделяет его.
|
||
|
||
## 12. Как Viewer собирает всю цепочку
|
||
|
||
Viewer начинает с последнего `TX + offset + size`:
|
||
|
||
```text
|
||
последний UserBlockchainChunk
|
||
↓
|
||
PreviousBlockchainChunkRef
|
||
↓
|
||
FULL reference table текущего big block
|
||
↓
|
||
TX предыдущего big block
|
||
↓
|
||
Range предыдущего chunk
|
||
↓
|
||
следующий backlink
|
||
↓
|
||
до NO_REFERENCE
|
||
```
|
||
|
||
Чужие chunks скачивать не требуется.
|
||
|
||
## 13. Минимальная настройка принимающего сервера
|
||
|
||
```properties
|
||
archive.publish.enabled=false
|
||
archive.import.allowedPublishers=server-a,server-b
|
||
archive.import.intervalMinutes=60
|
||
archive.import.workDir=data/archive-import
|
||
```
|
||
|
||
Если импорт архивов не нужен:
|
||
|
||
```properties
|
||
archive.import.allowedPublishers=
|
||
```
|
||
|
||
Тогда importer вообще не запускается.
|
||
|
||
## 14. Диагностика PostgreSQL
|
||
|
||
Pending archive heads:
|
||
|
||
```sql
|
||
SELECT login, archive_head_tx_id, archive_imported, archive_last_imported_tx_id
|
||
FROM solana_user_pda_current
|
||
WHERE is_server = TRUE
|
||
AND archive_head_tx_id <> ''
|
||
ORDER BY login;
|
||
```
|
||
|
||
Последние известные пользовательские chunks:
|
||
|
||
```sql
|
||
SELECT blockchain_name, publisher_login, arweave_tx_id,
|
||
big_block_number, chunk_offset, chunk_size, source_last_block_number
|
||
FROM archive_blockchain_location
|
||
ORDER BY updated_at_ms DESC;
|
||
```
|