Архитектура блокчейна: состояние как машина
Уроки о криптографии и распределённом консенсусе дали нам кирпичи: хеш, подпись, дерево Меркла, консенсус. Теперь из них собираем работающий узел. Восемь уроков, и в конце ты запускаешь локальную сеть, где узлы майнят, переводят монеты и сами разрешают форк. Начнём с одной идеи, на которую нанизано всё остальное: блокчейн это реплицированная машина состояний.
Идея
Сними мишуру с любого блокчейна, и под ней окажется до скуки знакомая вещь из теории автоматов (раздел 02-cs): машина состояний. Есть состояние (кто чем владеет) и функция перехода (применить блок транзакций). Подаёшь блоки по очереди, состояние меняется по строгому правилу.
Слово «реплицированная» это вся соль. Состояние держит не один сервер, а тысячи недоверяющих друг другу узлов, и у каждого своя копия. Чтобы копии не разъехались, переход обязан быть детерминированным до последнего бита. Тот же набор блоков на твоей машине и на моей обязан дать байт-в-байт одно состояние. Если хоть где-то закралась случайность, время или порядок обхода хеш-карты, узлы разойдутся в том, кто сколько должен, и сеть расколется.
Отсюда вся остальная конструкция выводится почти механически:
- Состояние. Кто чем владеет. У нас это балансы и счётчики
nonceпо адресам. - Транзакция. Подписанное намерение изменить состояние: «переведи 10 с моего адреса на адрес Боба».
- Блок. Пачка транзакций плюс заголовок, скреплённый PoW и ссылкой на предыдущий блок.
- Цепь. Блоки, связанные хешем в одну историю, плюс правило, какая ветка главная.
- Сеть. Узлы, которые гоняют друг другу блоки и транзакции и сходятся к одной истории.
Каждый из этих слоёв это один урок блока. Соберём их снизу вверх.
Состояние и переход
Возьмём аккаунтную модель, как у Ethereum (UTXO Bitcoin разберём в следующем уроке). Состояние это две карты: сколько у адреса монет и какой у него nonce, то есть сколько транзакций он уже отправил.
use std::collections::HashMap;
/// Аккаунтное состояние: балансы и счётчики nonce. Адресов, которых нет в
/// карте, как будто есть с балансом 0 и nonce 0.
#[derive(Clone, Default)]
pub struct State {
balances: HashMap<Address, u64>,
nonces: HashMap<Address, u64>,
}
impl State {
pub fn balance(&self, addr: &Address) -> u64 {
self.balances.get(addr).copied().unwrap_or(0)
}
/// Ожидаемый nonce следующей транзакции адреса.
pub fn nonce(&self, addr: &Address) -> u64 {
self.nonces.get(addr).copied().unwrap_or(0)
}
}
Функция перехода будет проверять подпись, сверять nonce, списывать средства и наращивать счётчик. Её мы напишем в уроке про мемпул, а пока держи в голове: каждая проверка в переходе это правило консенсуса. Ослабь любую (прими чужую подпись, разреши уйти в минус, прими повторный nonce), и твой узел форкнется от сети, потому что придёт к состоянию, с которым остальные не согласны.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.