Архитектура Ethereum: аккаунты, состояние, газ
Смарт-контракт исполняет не воздух: под ним конкретная машина с памятью, состоянием и счётчиком. Прежде чем лезть в EVM, разберём каркас: чем аккаунт-человек отличается от аккаунта-программы, что такое мировое состояние и почему его хранят в хитром дереве, как всё сериализуется и, главное, зачем нужен газ. Каждый ответ мы будем сравнивать с блокчейном, который собирали своими руками (урок 12).
Идея
У нашего узла состояние было простой таблицей «адрес, баланс и nonce» (урок 12). Ethereum устроен так же, но аккаунт богаче: у него бывает код. Отсюда два вида аккаунтов:
- Внешний аккаунт (EOA): им владеет ключ человека, у него есть баланс и nonce, но нет кода. Только он может начать транзакцию, подписав её. Это ровно адрес из нашего собственного узла.
- Контрактный аккаунт: у него вместо ключа код и своё хранилище. Сам транзакцию не начинает, только реагирует на вызов, исполняя код.
Состояние каждого аккаунта это четыре поля: nonce, balance, storageRoot и codeHash. Заметь: в самой записи аккаунта нет ни байткода, ни данных хранилища. Там лежат только хеш кода и корень дерева хранилища, а сами байты кода и слоты storage хранятся отдельно и подтягиваются по этим ссылкам (что за корень дерева, разберём в следующем разделе про MPT). У EOA codeHash это хеш пустой строки, а storageRoot это корень пустого дерева.
В нашей учебной модели мы это упростим и положим код и хранилище прямо в структуру, семантика от этого не меняется:
use std::collections::HashMap;
/// Аккаунт Ethereum. У внешнего аккаунта code и storage пусты, у контрактного
/// в code лежит байткод EVM, а в storage его постоянное состояние.
#[derive(Debug, Clone, Default)]
pub struct Account {
pub nonce: u64,
pub balance: U256,
pub code: Vec<u8>,
pub storage: HashMap<U256, U256>,
}
impl Account {
pub fn is_contract(&self) -> bool {
!self.code.is_empty()
}
}
/// Мировое состояние это отображение адреса в аккаунт. Всё, что есть в сети,
/// это одна большая карта, как таблица балансов нашего узла, только богаче.
pub type WorldState = HashMap<[u8; 20], Account>;
Мировое состояние это вся сеть разом: одна большая карта адрес в аккаунт. Каждый блок переводит её из одного состояния в другое, как переход состояния у нашего узла.
MPT: состояние в одном хеше
Тут Ethereum расходится с нами. Мы выводили состояние, проигрывая всю историю от genesis (функция state_at нашего узла). Ethereum так не может: аккаунтов десятки миллионов, проигрывать историю каждый раз немыслимо. Нужно и хранить состояние компактно, и уметь одним числом доказать «вот такое сейчас состояние».
Ответ это дерево Меркла-Патриции (MPT). Оно склеивает две идеи. Patricia trie это префиксное дерево: путь к аккаунту это его ключ, а общие префиксы ключей делят общие ветки. Одна тонкость: в дереве состояния ключ это не сам адрес, а keccak256 от него (такой вариант зовут secure trie: хеширование ключей не даёт злоумышленнику подобрать адреса так, чтобы перекосить дерево в длинную цепочку). Merkle значит, что каждый узел хешируется от детей (дерево Меркла из урока 01), поэтому всё дерево фиксируется одним корневым хешем.
Этот корень (stateRoot) лежит в заголовке блока, ровно как merkle_root транзакций у нас в уроке 14 и у Bitcoin в уроке 22. Поменялся один аккаунт, поменялся корень. И как SPV из урока 22, MPT позволяет доказать «у этого адреса такой баланс», предъявив путь из хешей, не таща всё дерево. В заголовке Ethereum таких корней три: состояние (stateRoot), транзакции блока (transactionsRoot) и чеки исполнения (receiptsRoot). А тот storageRoot, что мы видели в полях аккаунта, в заголовок не попадает: это корень отдельного дерева хранилища контракта, и лежит он внутри записи аккаунта в дереве состояния. Дерево в дереве: поменялся один storage-слот, поменялся storageRoot аккаунта, а за ним и stateRoot всей сети.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.