Перейти к содержанию

P2P синхронизация

Как работает P2P сеть и синхронизация блоков в ComputeChain.

Архитектура P2P

Компоненты

  1. P2P Node: Управляет соединениями с пирами
  2. Protocol: Формат сообщений (handshake, блоки, транзакции)
  3. Sync Engine: Логика синхронизации блоков
  4. Mempool: Распространение транзакций

Протокол сообщений

Типы сообщений

class P2PMessageType(str, Enum):
    HANDSHAKE = "handshake"
    NEW_BLOCK = "new_block"
    NEW_TX = "new_tx"
    GET_BLOCKS = "get_blocks"  # Запрос синхронизации
    BLOCKS_RESPONSE = "blocks_response"  # Ответ с блоками

Handshake

При подключении:

HandshakePayload(
    node_id="node-123",
    p2p_port=9000,
    protocol_version=1,
    network="devnet",
    best_height=15,
    best_hash="0x1234...",
    genesis_hash="0xabcd..."
)

Проверки:

  • network должен совпадать
  • genesis_hash должен совпадать
  • protocol_version должен быть совместим

Новый блок

Распространение:

NewBlockPayload(
    block={
        "header": {...},
        "txs": [...]
    }
)

Процесс:

  1. Валидатор производит блок
  2. Блок подписывается
  3. Блок рассылается всем пирам
  4. Пиры проверяют и добавляют блок

Новая транзакция

Распространение:

NewTxPayload(
    tx={
        "tx_type": "TRANSFER",
        "from_address": "cpc1...",
        ...
    }
)

Процесс:

  1. Транзакция попадает в мемпул
  2. Рассылается всем пирам
  3. Пиры добавляют в свой мемпул
  4. Валидатор включает в блок

Синхронизация блоков

Режимы синхронизации

SYNCING:

  • Нода отстаёт от сети
  • Не производит новые блоки
  • Запрашивает блоки у пиров

SYNCED:

  • Нода синхронизирована
  • Может производить блоки (если валидатор)
  • Принимает новые блоки от пиров

Процесс синхронизации

Шаг 1: Определение отставания

local_height = chain.height
peer_best_height = handshake.best_height

if peer_best_height > local_height:
    # Нужна синхронизация
    sync_state = SyncState.SYNCING

Шаг 2: Запрос блоков

GetBlocksPayload(
    from_height=local_height + 1,
    to_height=peer_best_height
)

Шаг 3: Получение блоков

BlocksResponsePayload(
    blocks=[
        {"header": {...}, "txs": [...]},
        {"header": {...}, "txs": [...]},
        ...
    ]
)

Шаг 4: Применение блоков

for block_data in blocks:
    block = Block.model_validate(block_data)
    if chain.add_block(block):
        # Блок добавлен успешно
        continue
    else:
        # Ошибка валидации
        break

Batch синхронизация

Оптимизация:

  • Блоки запрашиваются пачками (например, по 100)
  • Уменьшает количество сообщений
  • Ускоряет синхронизацию

Пример:

# Запрос блоков пачками
batch_size = 100
for start in range(local_height + 1, peer_best_height, batch_size):
    end = min(start + batch_size - 1, peer_best_height)
    blocks = request_blocks(start, end)
    apply_blocks(blocks)

Fork Resolution

Обнаружение форка

Признаки:

  • prev_hash блока не совпадает с локальным last_hash
  • Обнаружена более длинная валидная цепь

Процесс:

if block.prev_hash != chain.last_hash:
    # Возможен форк
    if len(peer_chain) > len(local_chain):
        # Откат локальной цепи
        rollback_to_height(fork_height)
        # Загрузка правильной ветки
        load_blocks_from_peer(peer_chain)

Rollback

Что происходит:

  1. Определение точки форка
  2. Откат локальных блоков до точки форка
  3. Откат стейта до точки форка
  4. Загрузка правильной ветки от пира
  5. Применение блоков правильной ветки

Пример:

Локальная цепь:  [0] -> [1] -> [2] -> [3] -> [4]
Правильная цепь: [0] -> [1] -> [2'] -> [3'] -> [4'] -> [5']

Форк на высоте 2:
1. Откат блоков 2, 3, 4
2. Загрузка блоков 2', 3', 4', 5'
3. Применение блоков

Управление пирами

Добавление пиров

При запуске:

./run_node.py --datadir .node_a run --peers 192.168.1.100:9000

Автоматически:

  • При handshake новый пир добавляется в список
  • Сохраняется в peers.json

Сохранение пиров

Файл: peers.json

Формат:

[
  "127.0.0.1:9001",
  "192.168.1.100:9000",
  "192.168.1.101:9000"
]

Обновление:

  • При подключении нового пира
  • При отключении пира (периодическая очистка)
  • При завершении ноды (сохранение текущего списка)

Переподключение

Автоматическое:

  • При запуске нода пытается подключиться к сохранённым пирам
  • При отключении пира нода пытается переподключиться
  • Экспоненциальная задержка между попытками

Примеры

Пример 1: Синхронизация новой ноды

1. Нода запускается с genesis
2. Подключается к пиру (best_height=100)
3. Определяет отставание: local=0, peer=100
4. Запрашивает блоки 1-100 пачками по 50
5. Применяет блоки последовательно
6. После синхронизации переходит в режим SYNCED

Пример 2: Обнаружение форка

1. Нода получает блок с prev_hash != last_hash
2. Запрашивает цепь у пира
3. Обнаруживает более длинную цепь
4. Откатывает локальные блоки
5. Загружает правильную ветку
6. Продолжает синхронизацию

Troubleshooting

Проблема: Нода не синхронизируется

Причины:

  • Нет подключений к пирам
  • Пиры недоступны
  • Firewall блокирует соединения

Решение:

# Проверить подключения
cat .node_a/peers.json

# Проверить доступность пира
telnet 192.168.1.100 9000

# Добавить пиры вручную
./run_node.py --datadir .node_a run --peers 192.168.1.100:9000

Проблема: Постоянные форки

Причины:

  • Неправильное время (time drift)
  • Разные genesis
  • Баги в консенсусе

Решение:

# Синхронизировать время
sudo ntpdate -s time.nist.gov

# Проверить genesis
cat .node_a/genesis.json | jq '.header'

# Пересобрать стейт
./run_node.py --datadir .node_a run --rebuild-state

Следующие шаги

  • Run Local — Запуск локальной ноды
  • Configuration — Настройка параметров
  • CLI Guide — Использование CLI