SHA256
Добавить импорт архивов с доверенных серверов
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
# Импорт доверенных 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;
|
||||
```
|
||||
Reference in New Issue
Block a user