• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Взгляд изнутри на блок EVM - SSTORE + SLOAD

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
Эта статья для тех кому инетересно внутреннее устройство блока Ethereum Virtual Mashine, для тех кто хочет более глубоко разобрать что же происходит на уровень ниже Solidity, и как с этим взаимодействовать. Для этого мы изучим архитектуру цепочки Ethereum, ее структуру данных и заглянем внутрь клиента «Go Ethereum» (Geth). Разберём данные, содержащиеся в блоке Ethereum, и погрузимся в "хранилище" конкретного контракта. Но для того чтобы лучше понять с наглядного изображения блока EVM:


blockEth.png

Только раз взглянув на него можно определить что к чему. Для этого достаточно перейти на 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.

1.png


State root - это корень дерева меркла в том смысле, что это хэш, который зависит от всех фрагментов данных, лежащих под ним. Если какая-либо часть данных изменится, корень также изменится. Структура данных под State root представляет собой Merkle Patricia Trie, в которой хранится пара «ключ-значение» для каждой учетной записи Ethereum в сети, где ключ — это хэш адреса Ethereum, а значение — объект учетной записи это учетная запись Ethereum, закодированная RLP. Учетная записть Eth адресс состоящий из 4 элементов:

  • Nonce - количество транзакций, совершенных аккаунтом
  • Balance - Баланс счета в Wei
  • Code Hash — хэш байт-кода, хранящегося в контракте/аккаунте.
  • Storage Root — keccak Hash корневого узла хранилища
Эти четыре пункта определяют место хранения смарт-контракта.
Лопаты взяли? Копаем глубже...

StateDB → stateObject → StateAccount

Необходимо усвоить и понять, что в аккаунте Ethereum есть 3 структуры:
  • StateAccount - это консенсусное представление "счетов Ethereum".
  • stateObject - модифицирующийся объект EthereumAccaunt
  • StateDB - используются для хранения информации в дереве Меркла, интерфейс запроса для получения: Ethereum accounts и контрактов.

stateObject входит в структуру StateDB что видно отсюда

Код:
// * 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
}
Переменная области, которая передается в функцию, содержит контекст контракта, такой как стек, память и т. д. Мы извлекаем 2 значения из стека и помечаем их loc (сокращение от местоположения) и val (сокращение от значения). Затем два значения, извлеченные из стека, используются в качестве входных данных вместе с адресом контракта для go-ethereum/core/state/statedb.go связанной с StateDB. Функция SetState использует адрес контракта, чтобы проверить, существует ли объект stateObject для этого контракта, если нет, он будет создан. Затем он вызывает SetState для этого stateObject, передавая в базу данных StateDB ключ и значение.
Код:
func (s *StateDB) SetState(addr common.Address, key, value common.Hash) {
    stateObject := s.GetOrNewStateObject(addr)
    if stateObject != nil {
        stateObject.SetState(s.db, key, value)
    }
}

Функция stateObject SetState выполняет некоторые проверки хранилища и того, изменилось ли значение, а затем запускает добавление записи в журнал, используется для отслеживания изменений состояния, чтобы их можно было отменить в случае исключения выполнения или запроса на изменение.

Код:
// 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)
}
После обновления журнала вызывается функция setState из storageObject с ключом и значением. Это обновляет файл storageObjects dirtyStorage.

Код:
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 скажешь?

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
}

Тип storage, соответствующий dirtyStorage, представляет собой простое сопоставление common.Hash с common.Hash.
Код:
type Storage map[common.Hash]common.Hash

Тип Hash — это просто массив байтов длины HashLength.
Код:
// Hash represents the 32 byte Keccak256 hash of arbitrary data.
type Hash [HashLength]byte

HashLength — это константа, определенная как 32 .
Код:
// 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
)

Ну поняли что это за сопоставление 32-байтового ключа с 32-байтовым значением.
Возможно, вы заметили 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
}
берем с вершины стека используя peek. Затем вызывается функция GetState для StateDB, передавая адрес контракта и место хранения. GetState получает объект stateObject, связанный с этим адресом контракта. Если stateObject не равен нулю, он вызывает GetState для этого stateObject.

Код:
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{}
}

Функция GetState в stateObject выполняет проверку fakeStorage, а затем проверяет dirtyStorage.
Если 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
    )

В противном случае вызовите функцию GetCommitedState, чтобы найти значение в дереве хранилища. Дальше проверяется наличие fakeStorage. Если pendingStorage существует, вернётся значение расположенное в отображении pendingStorage.
Код:
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
    )
Если ничего из вышеперечисленного не вернулось, выполнение перейдёт в originStorage и вернёт значение оттуда. Вы могли заметить, что функция сначала пыталась вернуть dirtyStorage, затем pendingStorage, а затем originStorage. Это имеет смысл, так как во время выполнения dirtyStorage является наиболее актуальным сопоставлением хранилища, за которым следует pending, а затем originStorage.
Одна транзакция может манипулировать одним слотом хранилища несколько раз, поэтому мы должны убедиться, что у нас самое последнее значение.
Давайте представим, что SSTORE происходит перед SLOAD в том же слоте и в той же транзакции. В этой ситуации dirtyStorage будет обновлен в SSTORE и возвращен в SLOAD.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх