[RU]

[EN]
Alphazine - Only Successful People.
ГАРАНТ ПОПОЛНИТЬ ДЕПОЗИТ


Вернуться   Alphazine - Only Successful People. > Общий > Криптовалюты. Блокчеин. ППМ. > Статьи.

Важная информация

[Определение часового пояса]
[ Регистрация ]
Имя:
Отделение людей от роботов


 
Опции темы
Взгляд изнутри на блок EVM - SSTORE + SLOAD
Старый   #1
Admin
Админ
 
Регистрация: 01.01.2020
Сообщений: 5,714
Спасибо: 172 раз(а)
Репутация:    
Депозит: 1.021 ?
Business Level: 25 ?
Отправить личное сообщение для Admin
Взгляд изнутри на блок 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 элементов:
  • 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.
  Ответить с цитированием
Опции темы
Опции просмотра