![]() |
|
||||
![]() |
#1 | |||
Админ
|
Взгляд изнутри на блок EVM - SSTORE + SLOAD
Эта статья для тех кому инетересно внутреннее устройство блока Ethereum Virtual Mashine, для тех кто хочет более глубоко разобрать что же происходит на уровень ниже Solidity, и как с этим взаимодействовать. Для этого мы изучим архитектуру цепочки Ethereum, ее структуру данных и заглянем внутрь клиента «Go Ethereum» (Geth). Разберём данные, содержащиеся в блоке Ethereum, и погрузимся в "хранилище" конкретного контракта. Но для того чтобы лучше понять с наглядного изображения блока EVM:
![]() Только раз взглянув на него можно определить что к чему. Для этого достаточно перейти на etherscan.io в раздел о блоках и сразу станет всё понятно. Для тех кто только проснулся и еще пьёт свой вкусный кофе разъясним. Заголовок блока содержит следующие поля: Prev Hash - Keccak-хэш родительского блока. Nonce - Используется при вычислении доказательства работы Timestamp - значение временной метки блока UNIX time( ). Uncles Hash - Uncle-блоки (или Ommer) создаются, когда два или более майнеров создают блоки почти одновременно. Только один блок может добыт и принят в качестве канонического в блокчейне. Остальные — это дяди-блоки, которые не включены, но тем не менее дают вознаграждение своим майнерам за проделанную работу. Beneficiary - Адрес бенефициара, получатель платы за майнинг LogsBloom - Фильтр Блума блока или транзакции представляет собой 2048-битную строку. Каждый журнал, созданный в транзакции, имеет от 0 до 4 тем. Каждая тема установит 3 бита в «1» на основе хэша темы. Позже, если вы захотите узнать, есть ли у блока или транзакции заданная тема в одном из журналов, вы можете проверить, установлены ли те же самые 3 бита. Если они не установлены, вы знаете, что тема не будет найдена ни в одном журнале транзакций. Если они установлены, вы можете догадаться, что они, вероятно, будут, но вам все равно нужно посмотреть журналы транзакций, чтобы убедиться, потому что фильтры Блума имеют риск ложных срабатываний. Difficulty - Скалярное значение сложности предыдущего блока Extra Data - 32 байта данных, относящихся к данному блоку Block Num - значение количества блоков-предшественников Gas Limit - значение текущего лимита использования газа на блок Gas Used - значение общего количества газа, потраченного на транзакции в данном блоке Mix Hash - 256-битное значение, используемое для подтверждения вычислений proof of work State Root - хэш всех балансов аккаунтов, хранилища контрактов, код контракта и одноразовые номера аккаунта. Хэш вычисляется с использованием дерева Меркла/Патриции. Transaction Root - хэш всех транзакций, включенных в этот блок Receipt Root - Хэш информации о получателе. А теперь сравним их с кодом клиента Geth. ![]() State root - это корень дерева меркла в том смысле, что это хэш, который зависит от всех фрагментов данных, лежащих под ним. Если какая-либо часть данных изменится, корень также изменится. Структура данных под State root представляет собой Merkle Patricia Trie, в которой хранится пара «ключ-значение» для каждой учетной записи Ethereum в сети, где ключ — это хэш адреса Ethereum, а значение — объект учетной записи это учетная запись Ethereum, закодированная RLP. Учетная записть Eth адресс состоящий из 4 элементов:
Лопаты взяли? Копаем глубже... StateDB → stateObject → StateAccount Необходимо усвоить и понять, что в аккаунте Ethereum есть 3 структуры:
Код:
Код: // * Accounts type StateDB struct { db Database prefetcher *triePrefetcher originalRoot common.Hash // The pre-state root, before any changes were made trie Trie hasher crypto.KeccakState snaps *snapshot.Tree snap snapshot.Snapshot snapDestructs map[common.Hash]struct{} snapAccounts map[common.Hash][]byte snapStorage map[common.Hash]map[common.Hash][]byte // This map holds 'live' objects, which will get modified while processing a state transition. stateObjects map[common.Address]*stateObject stateObjectsPending map[common.Address]struct{} // State objects finalized but not yet written to the trie stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution Код:
Код: // The usage pattern is as follows: // First you need to obtain a state object. // Account values can be accessed and modified through the object. // Finally, call CommitTrie to write the modified storage trie into a database. type stateObject struct { address common.Address addrHash common.Hash // hash of ethereum address of the account data types.StateAccount db *StateDB Код:
Код: // StateAccount is the Ethereum consensus representation of accounts. // These objects are stored in the main account trie. type StateAccount struct { Nonce uint64 Balance *big.Int Root common.Hash // merkle root of the storage trie CodeHash []byte } В структуре StateDB мы видим поле stateObjects, которое представляет собой сопоставление адресов с stateObjects (помните, что «state root» дерева Меркл представлял собой сопоставление адресов Ethereum с учетными записями Ethereum, а stateObject — это изменяемая учетная запись Ethereum). ) в stateObject struct видим поле данных типа StateAccount, а как мы говорили выше учетная запись Ethereum = StateAccount в Geth) Структурe StateAccount мы уже видели и она представляет учетную запись Ethereum, а поле Root это «state root». Теперь разберём как инициализируется учетная запись Ethereum. В StateDB есть функция createObject, которая создает новый stateObject и передает в него пустой StateAccount. Это фактически создает пустую «учетную запись Ethereum». Код:
Код: // newObject creates a state object. func newObject(db *StateDB, address common.Address, data types.StateAccount) *stateObject { if data.Balance == nil { data.Balance = new(big.Int) } if data.CodeHash == nil { data.CodeHash = emptyCodeHash } if data.Root == (common.Hash{}) { data.Root = emptyRoot } return &stateObject{ db: db, address: address, addrHash: crypto.Keccak256Hash(address[:]), data: data, originStorage: make(Storage), pendingStorage: make(Storage), dirtyStorage: make(Storage), } } Код:
Код: // the given address, it is overwritten and returned as the second return value. func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) { prev = s.getDeletedStateObject(addr) // Note, prev might have been deleted, we need that! var prevdestruct bool if s.snap != nil && prev != nil { _, prevdestruct = s.snapDestructs[prev.addrHash] if !prevdestruct { s.snapDestructs[prev.addrHash] = struct{}{} } } newobj = newObject(s, addr, types.StateAccount{}) if prev == nil { s.journal.append(createObjectChange{account: &addr}) } else { s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) } s.setStateObject(newobj) if prev != nil && !prev.deleted { return newobj, prev } return newobj, nil } В StateDB есть функция createObject, которая принимает адрес Ethereum и возвращает stateObject (помните, что stateObject представляет изменяемую учетную запись Ethereum). Функция createObject вызывает функцию newObject, передавая в stateDB адрес и пустой StateAccount (помните, что StateAccount = учетная запись Ethereum), она возвращает stateObject В операторе возврата функции newObject мы видим ряд полей, связанных с stateObject, адресом, данными, dirtyStorage и т. д.Поле данных stateObject сопоставляется с пустым входом StateAccount в функции. Обратите внимание, что нулевые значения заменяются в StateAccount. Возвращается созданный объект состояния, который содержит инициализированный StateAccount в качестве поля данных. Итак, у нас есть пустой stateAccount, что нам делать дальше? Мы хотим сохранить некоторые данные, и для этого нам нужно использовать код операции SSTORE. Прежде чем мы углубимся в реализацию SSTORE в Geth, давайте вспомним что делает SSTORE. Он извлекает 2 значения из стека, сначала 32-байтовый ключ, затем 32-байтовое значение и сохраняет это значение в указанном слоте памяти, определяемом ключом. Начнем с go-ethereum/core/vm/instructions.go который определяет все коды операций EVM. В этом файле мы находим функцию «opSstore». Код:
Код: func opSstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { if interpreter.readOnly { return nil, ErrWriteProtection } loc := scope.Stack.pop() val := scope.Stack.pop() interpreter.evm.StateDB.SetState(scope.Contract.Address(), loc.Bytes32(), val.Bytes32()) return nil, nil } Код:
Код: func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { stateObject.SetState(s.db, key, value) } } Код:
Код: // SetState updates a value in account storage. func (s *stateObject) SetState(db Database, key, value common.Hash) { // If the fake storage is set, put the temporary state update here. if s.fakeStorage != nil { s.fakeStorage[key] = value return } // If the new value is the same as old, don't set prev := s.GetState(db, key) if prev == value { return } // New value is different, update and journal the change s.db.journal.append(storageChange{ account: &s.address, key: key, prevalue: prev, }) s.setState(key, value) } Код:
Код: func (s *stateObject) finalise(prefetch bool) { slotsToPrefetch := make([][]byte, 0, len(s.dirtyStorage)) for key, value := range s.dirtyStorage { s.pendingStorage[key] = value if value != s.originStorage[key] { slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(key[:])) // Copy needed for closure } } if s.db.prefetcher != nil && prefetch && len(slotsToPrefetch) > 0 && s.data.Root != emptyRoot { s.db.prefetcher.prefetch(s.data.Root, slotsToPrefetch) } if len(s.dirtyStorage) > 0 { s.dirtyStorage = make(Storage) } } dirtyStorage определяется в stateObject имеет тип Storage и описывается как «Записи хранилища, которые были изменены в ходе выполнения текущей транзакции». Код:
Код: type stateObject struct { address common.Address addrHash common.Hash // hash of ethereum address of the account data types.StateAccount db *StateDB // DB error. // State objects are used by the consensus core and VM which are // unable to deal with database-level errors. Any error that occurs // during a database read is memoized here and will eventually be returned // by StateDB.Commit. dbErr error // Write caches. trie Trie // storage trie, which becomes non-nil on first access code Code // contract bytecode, which gets set when code is loaded originStorage Storage // Storage cache of original entries to dedup rewrites, reset for every transaction pendingStorage Storage // Storage entries that need to be flushed to disk, at the end of an entire block dirtyStorage Storage // Storage entries that have been modified in the current transaction execution fakeStorage Storage // Fake storage which constructed by caller for debugging purpose. // Cache flags. // When an object is marked suicided it will be delete from the trie // during the "update" phase of the state transition. dirtyCode bool // true if the code was updated suicided bool deleted bool } Код:
Код: type Storage map[common.Hash]common.Hash Код:
Код: // Hash represents the 32 byte Keccak256 hash of arbitrary data. type Hash [HashLength]byte Код:
Код: // Lengths of hashes and addresses in bytes. const ( // HashLength is the expected length of the hash HashLength = 32 // AddressLength is the expected length of the address AddressLength = 20 ) Возможно, вы заметили pendingStorage и originStorage в stateObject прямо над полем dirtyStorage. Все они связаны между собой, во время финализации dirtyStorage копируется в pendingStorage, который, в свою очередь, копируется в originStorage при обновлении дерева. После обновления дерева StateAccount также будет обновлен во время «фиксации» StateDB. Это записывает новое состояние в базовую базу данных в памяти. Okey, записывать мы научились, а как же загрузить из хранилища? Для этого существует опкод SLOAD. И сейчас мы посмотрим что у него "внутри". Для начала SLOAD извлекает значение из стека, 32-байтовый ключ, представляющий слот для хранения, и возвращает хранящееся там 32-байтовое значение. В файле instructions.go мы можем найти функцию «opSload». Код:
Код: func opSload(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { loc := scope.Stack.peek() hash := common.Hash(loc.Bytes32()) val := interpreter.evm.StateDB.GetState(scope.Contract.Address(), hash) loc.SetBytes(val.Bytes()) return nil, nil } Код:
Код: func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.GetState(s.db, hash) } return common.Hash{} } Если dirtyStorage существует, вернётся значение по ключу в отображении dirtyStorage. (dirtyStorage представляет самое актуальное состояние контракта, поэтому мы пытаемся сначала вернуть его) Код:
Код: func (s *stateObject) GetState(db Database, key common.Hash) common.Hash { // If the fake storage is set, only lookup the state here(in the debugging mode) if s.fakeStorage != nil { return s.fakeStorage[key] } // If we have a dirty value for this state entry, return it value, dirty := s.dirtyStorage[key] if dirty { return value } // Otherwise return the entry's original value return s.GetCommittedState(db, key) } // GetCommittedState retrieves a value from the committed account storage trie. func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Hash { // If the fake storage is set, only lookup the state here(in the debugging mode) if s.fakeStorage != nil { return s.fakeStorage[key] } // If we have a pending write or clean cached, return that if value, pending := s.pendingStorage[key]; pending { return value } if value, cached := s.originStorage[key]; cached { return value } // If no live objects are available, attempt to use snapshots var ( enc []byte err error ) Код:
Код: func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Hash { // If the fake storage is set, only lookup the state here(in the debugging mode) if s.fakeStorage != nil { return s.fakeStorage[key] } // If we have a pending write or clean cached, return that if value, pending := s.pendingStorage[key]; pending { return value } if value, cached := s.originStorage[key]; cached { return value } // If no live objects are available, attempt to use snapshots var ( enc []byte err error ) Одна транзакция может манипулировать одним слотом хранилища несколько раз, поэтому мы должны убедиться, что у нас самое последнее значение. Давайте представим, что SSTORE происходит перед SLOAD в том же слоте и в той же транзакции. В этой ситуации dirtyStorage будет обновлен в SSTORE и возвращен в SLOAD. |
|||
![]() |