P2P-сеть: gossip и распространение
Узел умеет майнить и проверять блоки, но он одинок. Чтобы получился блокчейн, узлам надо обмениваться блоками и транзакциями без центрального сервера и сходиться к одной истории. Разберём gossip, осознаем, что цепь это дерево, а не линия, и научим узлы разрешать конфликт самостоятельно.
Идея
В блокчейне нет главного сервера, который раздаёт правду. Есть равноправные узлы (peer-to-peer), и каждый знает лишь горстку соседей. Новость (блок или транзакция) должна доползти до всех через эти связи. Способ называется gossip: получил новое, проверил, переслал соседям; получил повтор, замолчал.
Транспорт тут вторичен. Реальные сети шлют данные по TCP или UDP или через libp2p. Но логика распространения и разрешения конфликтов от транспорта не зависит. Поэтому мы заменяем сокеты на прямые вызовы: детерминированная симуляция в памяти, набор узлов и широковещание. Сетевой транспорт ты уже собирал в разделе Rust, от проектирования протокола до надёжной доставки поверх UDP, и повторять его здесь значило бы отвлечься от главного.
Цепь это дерево, а не линия
Вот ключевой сдвиг в голове. Пока узел один, цепь это линия. Но в сети двое майнеров могут найти блок на одной высоте почти одновременно, и часть сети услышит первый, часть второй. Возникает форк: цепь расходится на две ветки. Значит, хранить «одну цепочку» наивно. На деле узел держит дерево блоков и отдельно решает, какая ветка главная.
Узел изнутри: где всё сходится
Прежде чем соединять узлы в сеть, соберём сам узел. Это ключевая интеграция всего проекта: дерево блоков, мемпул и транзакции здесь впервые работают вместе. Майнинг набирает транзакции из мемпула, печатает coinbase с наградой и собранными комиссиями, ищет nonce и применяет блок к собственной цепи; приём чужого блока прогоняет его через те же правила. Типы Blockchain, AddOutcome, ChainError и метод sync_mempool мы допишем в уроках 17 и 19 (константы BLOCK_REWARD и MAX_BLOCK_TXS, лимит транзакций в блоке, тоже живут там), но их роль ясна уже из сигнатур.
/// Узел сети: своя копия цепи и свой мемпул.
pub struct Node {
chain: Blockchain,
mempool: Mempool,
}
impl Node {
pub fn new(genesis: Block) -> Result<Node, ChainError> {
Ok(Node {
chain: Blockchain::new(genesis)?,
mempool: Mempool::new(),
})
}
pub fn tip(&self) -> Hash {
self.chain.tip()
}
pub fn height(&self) -> u64 {
self.chain.height()
}
pub fn balance(&self, addr: &Address) -> u64 {
self.chain.tip_state().balance(addr)
}
/// Принимает транзакцию от пользователя или из сети в мемпул, проверив её
/// относительно состояния на вершине.
pub fn submit_tx(&mut self, tx: Transaction) -> Result<Hash, MempoolError> {
let state = self.chain.tip_state();
self.mempool.add(tx, &state)
}
/// Майнит следующий блок: набирает транзакции из мемпула, добавляет coinbase
/// с наградой и комиссиями, ищет nonce под текущую сложность и применяет
/// блок к своей цепи. Возвращает намайненный блок для рассылки соседям.
pub fn mine_block(&mut self, miner: Address, timestamp: u64) -> Result<Block, ChainError> {
let tip = self.chain.tip();
let tip_block = self.chain.block(&tip).expect("вершина существует");
let height = tip_block.header.height + 1;
let bits = self.chain.expected_bits(tip_block);
let state = self.chain.tip_state();
let selected = self.mempool.select_for_block(&state, MAX_BLOCK_TXS - 1);
let fees: u64 = selected.iter().map(|t| t.fee).sum();
let mut txs = vec![Transaction::coinbase(miner, BLOCK_REWARD + fees, height)];
txs.extend(selected);
let mut block = Block::assemble(tip, height, bits, timestamp, txs);
mine(&mut block.header);
let outcome = self.chain.add_block(block.clone())?;
self.sync_mempool(&block, &outcome);
Ok(block)
}
/// Принимает блок из сети. Возвращает исход (или ошибку валидации, которую
/// gossip-слой использует, чтобы не ретранслировать мусор).
pub fn receive_block(&mut self, block: Block) -> Result<AddOutcome, ChainError> {
let outcome = self.chain.add_block(block.clone())?;
self.sync_mempool(&block, &outcome);
Ok(outcome)
}
}
Присмотрись к mine_block: coinbase печатает BLOCK_REWARD + fees, ровно столько, сколько разрешит проверка блока из следующего урока, а отбор оставляет одно место под саму coinbase (MAX_BLOCK_TXS - 1). И заметь симметрию: свой блок и чужой блок проходят через один и тот же add_block. Узел не доверяет даже себе, и это правильно: правило одно для всех.
Сеть как набор узлов
Теперь сеть. Она хранит узлы по индексам, а кто с кем «связан» в конкретный момент, задаёт вызывающий. Это позволит смоделировать и нормальную работу, и раздел сети.
/// Сеть узлов. Хранит узлы по индексам; кто с кем связан в конкретный момент,
/// задаёт вызывающий, передавая список получателей (так моделируется partition).
pub struct Network {
nodes: Vec<Node>,
}
impl Network {
/// Поднимает count узлов из одного и того же genesis-блока.
pub fn new(genesis: Block, count: usize) -> Network {
let nodes = (0..count)
.map(|_| Node::new(genesis.clone()).expect("genesis валиден"))
.collect();
Network { nodes }
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn node(&self, i: usize) -> &Node {
&self.nodes[i]
}
pub fn node_mut(&mut self, i: usize) -> &mut Node {
&mut self.nodes[i]
}
}
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.