Commit e5fc3fcf authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

feat: storage slot setting + memory db (#3216)

* state-surgery: add solc package

Move types into solc package that are associated
with solc. Add `CompilerInput` and `CompilerOutput`
types.

* state-surgery: clean up hardhat package

Use some solc types

* state-surgery: add state package

Implement some of smock's utils in go

* state-surgery: cleanup

* state-surgery: refactor + add better test coverage

* state-surgery: cleanup

* state-surgery: add tests for merging storage slots

* state-surgery: godoc

* state-surgery: more tests

* state-surgery: more cleanup
parent e4ef92a6
surgery: surgery:
go build -o ./surgery ./cmd/main.go go build -o ./surgery ./cmd/main.go
.PHONY: surgery
test:
go test ./...
.PHONY: surgery test
...@@ -8,6 +8,8 @@ import ( ...@@ -8,6 +8,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"github.com/ethereum-optimism/optimism/state-surgery/solc"
) )
// `Hardhat` encapsulates all of the functionality required to interact // `Hardhat` encapsulates all of the functionality required to interact
...@@ -75,11 +77,11 @@ func (h *Hardhat) initDeployments() error { ...@@ -75,11 +77,11 @@ func (h *Hardhat) initDeployments() error {
if strings.Contains(path, "solcInputs") { if strings.Contains(path, "solcInputs") {
return nil return nil
} }
if !strings.HasSuffix(path, ".json") {
name := filepath.Join(deploymentPath, h.network, path)
if !strings.HasSuffix(name, ".json") {
return nil return nil
} }
name := filepath.Join(deploymentPath, h.network, path)
file, err := os.ReadFile(name) file, err := os.ReadFile(name)
if err != nil { if err != nil {
return err return err
...@@ -247,3 +249,23 @@ func (h *Hardhat) GetBuildInfo(name string) (*BuildInfo, error) { ...@@ -247,3 +249,23 @@ func (h *Hardhat) GetBuildInfo(name string) (*BuildInfo, error) {
return buildInfos[0], nil return buildInfos[0], nil
} }
// TODO(tynes): handle fully qualified names properly
func (h *Hardhat) GetStorageLayout(name string) (*solc.StorageLayout, error) {
fqn := ParseFullyQualifiedName(name)
buildInfo, err := h.GetBuildInfo(name)
if err != nil {
return nil, err
}
for _, source := range buildInfo.Output.Contracts {
for name, contract := range source {
if name == fqn.ContractName {
return &contract.StorageLayout, nil
}
}
}
return nil, fmt.Errorf("contract not found for %s", fqn.ContractName)
}
...@@ -132,6 +132,8 @@ func TestHardhatGetBuildInfo(t *testing.T) { ...@@ -132,6 +132,8 @@ func TestHardhatGetBuildInfo(t *testing.T) {
} }
func TestHardhatGetDeployments(t *testing.T) { func TestHardhatGetDeployments(t *testing.T) {
t.Parallel()
hh, err := hardhat.New( hh, err := hardhat.New(
"goerli", "goerli",
[]string{"testdata/artifacts"}, []string{"testdata/artifacts"},
...@@ -143,3 +145,18 @@ func TestHardhatGetDeployments(t *testing.T) { ...@@ -143,3 +145,18 @@ func TestHardhatGetDeployments(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
require.NotNil(t, deployment) require.NotNil(t, deployment)
} }
func TestHardhatGetStorageLayout(t *testing.T) {
t.Parallel()
hh, err := hardhat.New(
"goerli",
[]string{"testdata/artifacts"},
[]string{"testdata/deployments"},
)
require.Nil(t, err)
storageLayout, err := hh.GetStorageLayout("HelloWorld")
require.Nil(t, err)
require.NotNil(t, storageLayout)
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{ {
"_format": "hh-sol-dbg-1", "_format": "hh-sol-dbg-1",
"buildInfo": "../../build-info/c5729209e616d57e62ada8bf0034436e.json" "buildInfo": "../../build-info/41b5106372a301360350245ee188494f.json"
} }
...@@ -8,6 +8,51 @@ ...@@ -8,6 +8,51 @@
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "constructor" "type": "constructor"
}, },
{
"inputs": [],
"name": "addr",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "addresses",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "boolean",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "gm", "name": "gm",
...@@ -21,6 +66,19 @@ ...@@ -21,6 +66,19 @@
"stateMutability": "nonpayable", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "small",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "time", "name": "time",
...@@ -35,8 +93,8 @@ ...@@ -35,8 +93,8 @@
"type": "function" "type": "function"
} }
], ],
"bytecode": "0x608060405234801561001057600080fd5b504260008190555060ed806100266000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c806316ada547146037578063c0129d43146051575b600080fd5b603d606b565b60405160489190609e565b60405180910390f35b60576071565b60405160629190609e565b60405180910390f35b60005481565b6000806000549050426000819055508091505090565b6000819050919050565b6098816087565b82525050565b600060208201905060b160008301846091565b9291505056fea2646970667358221220880ac624623135a410271b0c719fe0f86b85ff1eb258d2f766c58f2e87907b8864736f6c634300080f0033", "bytecode": "0x608060405234801561001057600080fd5b504260005561014d806100246000396000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c806316ada547146100675780636cf3c25e14610083578063767800de146100a2578063c0129d43146100cd578063c5b57bdb146100da578063edf26d9b146100fe575b600080fd5b61007060005481565b6040519081526020015b60405180910390f35b6003546100909060ff1681565b60405160ff909116815260200161007a565b6001546100b5906001600160a01b031681565b6040516001600160a01b03909116815260200161007a565b6000805442909155610070565b6001546100ee90600160a01b900460ff1681565b604051901515815260200161007a565b6100b561010c366004610127565b6002602052600090815260409020546001600160a01b031681565b60006020828403121561013957600080fd5b503591905056fea164736f6c634300080f000a",
"deployedBytecode": "0x6080604052348015600f57600080fd5b506004361060325760003560e01c806316ada547146037578063c0129d43146051575b600080fd5b603d606b565b60405160489190609e565b60405180910390f35b60576071565b60405160629190609e565b60405180910390f35b60005481565b6000806000549050426000819055508091505090565b6000819050919050565b6098816087565b82525050565b600060208201905060b160008301846091565b9291505056fea2646970667358221220880ac624623135a410271b0c719fe0f86b85ff1eb258d2f766c58f2e87907b8864736f6c634300080f0033", "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100625760003560e01c806316ada547146100675780636cf3c25e14610083578063767800de146100a2578063c0129d43146100cd578063c5b57bdb146100da578063edf26d9b146100fe575b600080fd5b61007060005481565b6040519081526020015b60405180910390f35b6003546100909060ff1681565b60405160ff909116815260200161007a565b6001546100b5906001600160a01b031681565b6040516001600160a01b03909116815260200161007a565b6000805442909155610070565b6001546100ee90600160a01b900460ff1681565b604051901515815260200161007a565b6100b561010c366004610127565b6002602052600090815260409020546001600160a01b031681565b60006020828403121561013957600080fd5b503591905056fea164736f6c634300080f000a",
"linkReferences": {}, "linkReferences": {},
"deployedLinkReferences": {} "deployedLinkReferences": {}
} }
...@@ -3,6 +3,7 @@ package hardhat ...@@ -3,6 +3,7 @@ package hardhat
import ( import (
"encoding/json" "encoding/json"
"github.com/ethereum-optimism/optimism/state-surgery/solc"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
...@@ -20,7 +21,7 @@ type Deployment struct { ...@@ -20,7 +21,7 @@ type Deployment struct {
Metadata string `json:"metadata"` Metadata string `json:"metadata"`
Receipt Receipt `json:"receipt"` Receipt Receipt `json:"receipt"`
SolcInputHash string `json:"solcInputHash"` SolcInputHash string `json:"solcInputHash"`
StorageLayout StorageLayout `json:"storageLayout"` StorageLayout solc.StorageLayout `json:"storageLayout"`
TransactionHash common.Hash `json:"transactionHash"` TransactionHash common.Hash `json:"transactionHash"`
Userdoc json.RawMessage `json:"userdoc"` Userdoc json.RawMessage `json:"userdoc"`
} }
...@@ -55,31 +56,6 @@ type Log struct { ...@@ -55,31 +56,6 @@ type Log struct {
Blockhash common.Hash `json:"blockHash"` Blockhash common.Hash `json:"blockHash"`
} }
// StorageLayout represents the storage layout of a contract
type StorageLayout struct {
Storage []StorageLayoutEntry `json:"storage"`
Types map[string]StorageLayoutType `json:"types"`
}
// StorageLayoutEntry represents a single entry in the StorageLayout
type StorageLayoutEntry struct {
AstId uint `json:"astId"`
Contract string `json:"contract"`
Label string `json:"label"`
Offset uint `json:"offset"`
Slot uint `json:"slot,string"`
Type string `json"type"`
}
// StorageLayoutType represents the type of storage layout
type StorageLayoutType struct {
Encoding string `json:"encoding"`
Label string `json:"label"`
NumberOfBytes string `json:"numberOfBytes"`
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
}
// Artifact represents a hardhat compilation artifact // Artifact represents a hardhat compilation artifact
type Artifact struct { type Artifact struct {
Format string `json:"_format"` Format string `json:"_format"`
...@@ -118,6 +94,6 @@ type BuildInfo struct { ...@@ -118,6 +94,6 @@ type BuildInfo struct {
Id string `json:"id"` Id string `json:"id"`
SolcVersion string `json:"solcVersion"` SolcVersion string `json:"solcVersion"`
SolcLongVersion string `json:"solcLongVersion"` SolcLongVersion string `json:"solcLongVersion"`
Input json.RawMessage `json:"input"` Input solc.CompilerInput `json:"input"`
Output json.RawMessage `json:"output"` Output solc.CompilerOutput `json:"output"`
} }
package solc
import (
"encoding/json"
"github.com/ethereum-optimism/optimism/l2geth/accounts/abi"
)
type CompilerInput struct {
Language string `json:"language"`
Sources map[string]map[string]string `json:"sources"`
Settings CompilerSettings `json:"settings"`
}
type CompilerSettings struct {
Optimizer OptimizerSettings `json:"optimizer"`
Metadata CompilerInputMetadata `json:"metadata"`
OutputSelection map[string]map[string][]string `json:"outputSelection"`
EvmVersion string `json:"evmVersion,omitempty"`
Libraries map[string]map[string]string `json:"libraries,omitempty"`
}
type OptimizerSettings struct {
Enabled bool `json:"enabled"`
Runs uint `json:"runs"`
}
type CompilerInputMetadata struct {
UseLiteralContent bool `json:"useLiteralContent"`
}
type CompilerOutput struct {
Contracts map[string]CompilerOutputContracts `json:"contracts"`
Sources CompilerOutputSources `json:"sources"`
}
type CompilerOutputContracts map[string]CompilerOutputContract
// TODO(tynes): ignoring devdoc and userdoc for now
type CompilerOutputContract struct {
Abi abi.ABI `json:"abi"`
Evm CompilerOutputEvm `json:"evm"`
Metadata string `json:"metadata"`
StorageLayout StorageLayout `json:"storageLayout"`
}
type StorageLayout struct {
Storage []StorageLayoutEntry `json:"storage"`
Types map[string]StorageLayoutType `json:"types"`
}
type StorageLayoutEntry struct {
AstId uint `json:"astId"`
Contract string `json:"contract"`
Label string `json:"label"`
Offset uint `json:"offset"`
Slot uint `json:"slot,string"`
Type string `json"type"`
}
type StorageLayoutType struct {
Encoding string `json:"encoding"`
Label string `json:"label"`
NumberOfBytes uint `json:"numberOfBytes,string"`
Key string `json:"key,omitempty"`
Value string `json:"value,omitempty"`
}
type CompilerOutputEvm struct {
Bytecode CompilerOutputBytecode `json:"bytecode"`
DeployedBytecode CompilerOutputBytecode `json:"deployedBytecode"`
GasEstimates map[string]map[string]string `json:"gasEstimates"`
MethodIdentifiers map[string]string `json:"methodIdentifiers"`
}
// Object must be a string because its not guranteed to be
// a hex string
type CompilerOutputBytecode struct {
Object string `json:"object"`
Opcodes string `json:"opcodes"`
SourceMap string `json:"sourceMap"`
LinkReferences LinkReferences `json:"linkReferences"`
}
type LinkReferences map[string]LinkReference
type LinkReference map[string][]LinkReferenceOffset
type LinkReferenceOffset struct {
Length uint `json:"length"`
Start uint `json:"start"`
}
type CompilerOutputSources map[string]CompilerOutputSource
type CompilerOutputSource struct {
Id uint `json:"id"`
Ast Ast `json:"ast"`
}
type Ast struct {
AbsolutePath string `json:"absolutePath"`
ExportedSymbols map[string][]uint `json:"exportedSymbols"`
Id uint `json:"id"`
License string `json:"license"`
NodeType string `json:"nodeType"`
Nodes json.RawMessage `json:"nodes"`
}
package state
import (
"errors"
"fmt"
"math/big"
"reflect"
"strings"
"github.com/ethereum-optimism/optimism/state-surgery/solc"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
// EncodeStorageKeyValue encodes the key value pair that is stored in state
// given a StorageLayoutEntry and StorageLayoutType. A single input may result
// in multiple outputs. Unknown or unimplemented types will return an error.
// Note that encoding uints is *not* overflow safe, so be sure to check
// the ABI before setting very large values
func EncodeStorageKeyValue(value any, entry solc.StorageLayoutEntry, storageType solc.StorageLayoutType) ([]*EncodedStorage, error) {
label := storageType.Label
encoded := make([]*EncodedStorage, 0)
switch storageType.Encoding {
case "inplace":
key := encodeSlotKey(entry)
switch label {
case "bool":
val, err := EncodeBoolValue(value, entry.Offset)
if err != nil {
return nil, err
}
encoded = append(encoded, &EncodedStorage{key, val})
case "address":
val, err := EncodeAddressValue(value, entry.Offset)
if err != nil {
return nil, err
}
encoded = append(encoded, &EncodedStorage{key, val})
case "bytes":
return nil, fmt.Errorf("%w: %s", errUnimplemented, label)
default:
switch true {
case strings.HasPrefix(label, "contract"):
val, err := EncodeAddressValue(value, entry.Offset)
if err != nil {
return nil, err
}
encoded = append(encoded, &EncodedStorage{key, val})
case strings.HasPrefix(label, "uint"):
val, err := EncodeUintValue(value, entry.Offset)
if err != nil {
return nil, err
}
encoded = append(encoded, &EncodedStorage{key, val})
default:
// structs are not supported
return nil, fmt.Errorf("%w: %s", errUnimplemented, label)
}
}
case "dynamic_array":
case "bytes":
return nil, fmt.Errorf("%w: %s", errUnimplemented, label)
case "mapping":
if strings.HasPrefix(storageType.Value, "mapping") {
return nil, fmt.Errorf("%w: %s", errUnimplemented, "nested mappings")
}
values, ok := value.(map[any]any)
if !ok {
return nil, fmt.Errorf("cannot parse mapping")
}
keyEncoder, err := getElementEncoder(storageType.Key)
if err != nil {
return nil, err
}
valueEncoder, err := getElementEncoder(storageType.Value)
if err != nil {
return nil, err
}
// Mapping values have 0 offset
for rawKey, rawVal := range values {
encodedKey, err := keyEncoder(rawKey, 0)
if err != nil {
return nil, err
}
encodedSlot := encodeSlotKey(entry)
preimage := [64]byte{}
copy(preimage[0:32], encodedKey.Bytes())
copy(preimage[32:64], encodedSlot.Bytes())
hash := crypto.Keccak256(preimage[:])
key := common.BytesToHash(hash)
val, err := valueEncoder(rawVal, 0)
if err != nil {
return nil, err
}
encoded = append(encoded, &EncodedStorage{key, val})
}
default:
return nil, fmt.Errorf("unknown encoding: %s", storageType.Encoding)
}
return encoded, nil
}
// encodeSlotKey will encode the storage slot key. This does not
// support mappings.
func encodeSlotKey(entry solc.StorageLayoutEntry) common.Hash {
slot := new(big.Int).SetUint64(uint64(entry.Slot))
return common.BigToHash(slot)
}
// ElementEncoder is a function that can encode an element
// based on a solidity type
type ElementEncoder func(value any, offset uint) (common.Hash, error)
// getElementEncoder will return the correct ElementEncoder
// given a solidity type.
func getElementEncoder(kind string) (ElementEncoder, error) {
switch kind {
case "t_address":
return EncodeAddressValue, nil
case "t_bool":
return EncodeBoolValue, nil
default:
if strings.HasPrefix(kind, "t_uint") {
return EncodeUintValue, nil
}
}
return nil, fmt.Errorf("unsupported type: %s", kind)
}
// EncodeBoolValue will encode a boolean value given a storage
// offset.
func EncodeBoolValue(value any, offset uint) (common.Hash, error) {
val, err := encodeBoolValue(value)
if err != nil {
return common.Hash{}, err
}
return handleOffset(val, offset), nil
}
// encodeBoolValue will encode a boolean value into a type
// suitable for solidity storage.
func encodeBoolValue(value any) (common.Hash, error) {
name := reflect.TypeOf(value).Name()
switch name {
case "bool":
boolean, ok := value.(bool)
if !ok {
return common.Hash{}, errInvalidType
}
if boolean {
return common.BigToHash(common.Big1), nil
} else {
return common.Hash{}, nil
}
case "string":
boolean, ok := value.(string)
if !ok {
return common.Hash{}, errInvalidType
}
if boolean == "true" {
return common.BigToHash(common.Big1), nil
} else {
return common.Hash{}, nil
}
default:
return common.Hash{}, errInvalidType
}
}
// EncodeAddressValue will encode an address like value given a
// storage offset.
func EncodeAddressValue(value any, offset uint) (common.Hash, error) {
val, err := encodeAddressValue(value)
if err != nil {
return common.Hash{}, err
}
return handleOffset(val, offset), nil
}
// encodeAddressValue will encode an address value into
// a type suitable for solidity storage.
func encodeAddressValue(value any) (common.Hash, error) {
name := reflect.TypeOf(value).Name()
switch name {
case "Address":
address, ok := value.(common.Address)
if !ok {
return common.Hash{}, errInvalidType
}
return address.Hash(), nil
case "string":
address, ok := value.(string)
if !ok {
return common.Hash{}, errInvalidType
}
return common.HexToAddress(address).Hash(), nil
default:
return common.Hash{}, errInvalidType
}
}
// EncodeUintValue will encode a uint value given a storage offset
func EncodeUintValue(value any, offset uint) (common.Hash, error) {
val, err := encodeUintValue(value)
if err != nil {
return common.Hash{}, err
}
return handleOffset(val, offset), nil
}
// encodeUintValue will encode a uint like type into a
// type suitable for solidity storage.
func encodeUintValue(value any) (common.Hash, error) {
val := reflect.ValueOf(value)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
name := val.Type().Name()
switch name {
case "uint":
val, ok := value.(uint)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64((uint64(val)))
return common.BigToHash(result), nil
case "int":
val, ok := value.(int)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(uint64(val))
return common.BigToHash(result), nil
case "uint64":
val, ok := value.(uint64)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(val)
return common.BigToHash(result), nil
case "uint32":
val, ok := value.(uint32)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(uint64(val))
return common.BigToHash(result), nil
case "uint16":
val, ok := value.(uint16)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(uint64(val))
return common.BigToHash(result), nil
case "uint8":
val, ok := value.(uint8)
if !ok {
return common.Hash{}, errInvalidType
}
result := new(big.Int).SetUint64(uint64(val))
return common.BigToHash(result), nil
case "string":
val, ok := value.(string)
if !ok {
return common.Hash{}, errInvalidType
}
number, err := hexutil.DecodeBig(val)
if err != nil {
if errors.Is(err, hexutil.ErrMissingPrefix) {
number, ok = new(big.Int).SetString(val, 10)
if !ok {
return common.Hash{}, errInvalidType
}
} else if errors.Is(err, hexutil.ErrLeadingZero) {
number, ok = new(big.Int).SetString(val[2:], 16)
if !ok {
return common.Hash{}, errInvalidType
}
}
}
return common.BigToHash(number), nil
case "Int":
val, ok := value.(*big.Int)
if !ok {
return common.Hash{}, errInvalidType
}
return common.BigToHash(val), nil
default:
return common.Hash{}, errInvalidType
}
}
// handleOffset will offset a value in storage by shifting
// it to the left. This is useful for when multiple variables
// are tightly packed in a storage slot.
func handleOffset(hash common.Hash, offset uint) common.Hash {
if offset == 0 {
return hash
}
number := hash.Big()
shifted := new(big.Int).Lsh(number, offset*8)
return common.BigToHash(shifted)
}
package state
import (
"bytes"
"fmt"
"math/big"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
var _ vm.StateDB = (*MemoryStateDB)(nil)
var emptyCodeHash = crypto.Keccak256(nil)
// MemoryStateDB implements geth's StateDB interface
// but operates on a core.Genesis so that a genesis.json
// can easily be created.
type MemoryStateDB struct {
rw sync.RWMutex
genesis *core.Genesis
}
func NewMemoryStateDB(genesis *core.Genesis) *MemoryStateDB {
if genesis == nil {
genesis = core.DeveloperGenesisBlock(15, 15_000_000, common.Address{})
}
return &MemoryStateDB{
genesis: genesis,
rw: sync.RWMutex{},
}
}
// Genesis is a getter for the underlying core.Genesis
func (db *MemoryStateDB) Genesis() *core.Genesis {
return db.genesis
}
// GetAccount is a getter for a core.GenesisAccount found in
// the core.Genesis
func (db *MemoryStateDB) GetAccount(addr common.Address) *core.GenesisAccount {
db.rw.RLock()
defer db.rw.RUnlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return nil
}
return &account
}
// StateDB interface implemented below
func (db *MemoryStateDB) CreateAccount(addr common.Address) {
db.rw.Lock()
defer db.rw.Unlock()
db.genesis.Alloc[addr] = core.GenesisAccount{
Code: []byte{},
Storage: make(map[common.Hash]common.Hash),
Balance: big.NewInt(0),
Nonce: 0,
}
}
func (db *MemoryStateDB) SubBalance(addr common.Address, amount *big.Int) {
db.rw.Lock()
defer db.rw.Unlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
panic(fmt.Sprintf("%s not in state", addr))
}
if account.Balance.Sign() == 0 {
return
}
account.Balance = new(big.Int).Sub(account.Balance, amount)
db.genesis.Alloc[addr] = account
}
func (db *MemoryStateDB) AddBalance(addr common.Address, amount *big.Int) {
db.rw.Lock()
defer db.rw.Unlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
panic(fmt.Sprintf("%s not in state", addr))
}
account.Balance = new(big.Int).Add(account.Balance, amount)
db.genesis.Alloc[addr] = account
}
func (db *MemoryStateDB) GetBalance(addr common.Address) *big.Int {
db.rw.RLock()
defer db.rw.RUnlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return common.Big0
}
return account.Balance
}
func (db *MemoryStateDB) GetNonce(addr common.Address) uint64 {
db.rw.RLock()
defer db.rw.RUnlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return 0
}
return account.Nonce
}
func (db *MemoryStateDB) SetNonce(addr common.Address, value uint64) {
db.rw.Lock()
defer db.rw.Unlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return
}
account.Nonce = value
db.genesis.Alloc[addr] = account
}
func (db *MemoryStateDB) GetCodeHash(addr common.Address) common.Hash {
db.rw.RLock()
defer db.rw.RUnlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return common.Hash{}
}
if len(account.Code) == 0 {
return common.BytesToHash(emptyCodeHash)
}
return common.BytesToHash(crypto.Keccak256(account.Code))
}
func (db *MemoryStateDB) GetCode(addr common.Address) []byte {
db.rw.RLock()
defer db.rw.RUnlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return nil
}
if bytes.Equal(crypto.Keccak256(account.Code), emptyCodeHash) {
return nil
}
return account.Code
}
func (db *MemoryStateDB) SetCode(addr common.Address, code []byte) {
db.rw.Lock()
defer db.rw.Unlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return
}
account.Code = code
db.genesis.Alloc[addr] = account
}
func (db *MemoryStateDB) GetCodeSize(addr common.Address) int {
db.rw.Lock()
defer db.rw.Unlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return 0
}
if bytes.Equal(crypto.Keccak256(account.Code), emptyCodeHash) {
return 0
}
return len(account.Code)
}
func (db *MemoryStateDB) AddRefund(uint64) {
panic("AddRefund unimplemented")
}
func (db *MemoryStateDB) SubRefund(uint64) {
panic("SubRefund unimplemented")
}
func (db *MemoryStateDB) GetRefund() uint64 {
panic("GetRefund unimplemented")
}
func (db *MemoryStateDB) GetCommittedState(common.Address, common.Hash) common.Hash {
panic("GetCommittedState unimplemented")
}
func (db *MemoryStateDB) GetState(addr common.Address, key common.Hash) common.Hash {
db.rw.RLock()
defer db.rw.RUnlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return common.Hash{}
}
return account.Storage[key]
}
func (db *MemoryStateDB) SetState(addr common.Address, key, value common.Hash) {
db.rw.Lock()
defer db.rw.Unlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return
}
account.Storage[key] = value
db.genesis.Alloc[addr] = account
}
func (db *MemoryStateDB) Suicide(common.Address) bool {
panic("Suicide unimplemented")
}
func (db *MemoryStateDB) HasSuicided(common.Address) bool {
panic("HasSuicided unimplemented")
}
// Exist reports whether the given account exists in state.
// Notably this should also return true for suicided accounts.
func (db *MemoryStateDB) Exist(addr common.Address) bool {
db.rw.RLock()
defer db.rw.RUnlock()
_, ok := db.genesis.Alloc[addr]
return ok
}
// Empty returns whether the given account is empty. Empty
// is defined according to EIP161 (balance = nonce = code = 0).
func (db *MemoryStateDB) Empty(addr common.Address) bool {
db.rw.RLock()
defer db.rw.RUnlock()
account, ok := db.genesis.Alloc[addr]
isZeroNonce := account.Nonce == 0
isZeroValue := account.Balance.Sign() == 0
isEmptyCode := bytes.Equal(crypto.Keccak256(account.Code), emptyCodeHash)
return ok || (isZeroNonce && isZeroValue && isEmptyCode)
}
func (db *MemoryStateDB) PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) {
panic("PrepareAccessList unimplemented")
}
func (db *MemoryStateDB) AddressInAccessList(addr common.Address) bool {
panic("AddressInAccessList unimplemented")
}
func (db *MemoryStateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) {
panic("SlotInAccessList unimplemented")
}
// AddAddressToAccessList adds the given address to the access list. This operation is safe to perform
// even if the feature/fork is not active yet
func (db *MemoryStateDB) AddAddressToAccessList(addr common.Address) {
panic("AddAddressToAccessList unimplemented")
}
// AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform
// even if the feature/fork is not active yet
func (db *MemoryStateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) {
panic("AddSlotToAccessList unimplemented")
}
func (db *MemoryStateDB) RevertToSnapshot(int) {
panic("RevertToSnapshot unimplemented")
}
func (db *MemoryStateDB) Snapshot() int {
panic("Snapshot unimplemented")
}
func (db *MemoryStateDB) AddLog(*types.Log) {
panic("AddLog unimplemented")
}
func (db *MemoryStateDB) AddPreimage(common.Hash, []byte) {
panic("AddPreimage unimplemented")
}
func (db *MemoryStateDB) ForEachStorage(addr common.Address, cb func(common.Hash, common.Hash) bool) error {
db.rw.RLock()
defer db.rw.RUnlock()
account, ok := db.genesis.Alloc[addr]
if !ok {
return nil
}
for key, value := range account.Storage {
if !cb(key, value) {
return nil
}
}
return nil
}
package state_test
import (
"math/big"
"math/rand"
"testing"
"time"
"github.com/ethereum-optimism/optimism/state-surgery/state"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
func TestAddBalance(t *testing.T) {
t.Parallel()
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
db := state.NewMemoryStateDB(nil)
for i := 0; i < 100; i++ {
key, _ := crypto.GenerateKey()
addr := crypto.PubkeyToAddress(key.PublicKey)
value := new(big.Int).Rand(rng, big.NewInt(1000))
db.CreateAccount(addr)
db.AddBalance(addr, value)
account := db.GetAccount(addr)
require.NotNil(t, account)
require.Equal(t, account.Balance, value)
}
}
func TestCode(t *testing.T) {
t.Parallel()
db := state.NewMemoryStateDB(nil)
for i := 0; i < 100; i++ {
key, _ := crypto.GenerateKey()
addr := crypto.PubkeyToAddress(key.PublicKey)
db.CreateAccount(addr)
pre := db.GetCode(addr)
require.Nil(t, pre)
code := make([]byte, rand.Intn(1024))
rand.Read(code)
db.SetCode(addr, code)
post := db.GetCode(addr)
require.Equal(t, post, code)
size := db.GetCodeSize(addr)
require.Equal(t, size, len(code))
codeHash := db.GetCodeHash(addr)
require.Equal(t, codeHash, common.BytesToHash(crypto.Keccak256(code)))
}
}
package state
import (
"errors"
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/state-surgery/solc"
"github.com/ethereum/go-ethereum/common"
)
// StorageValues represents the values to be set in storage.
// The key is the name of the storage variable and the value
// is the value to set in storage.
type StorageValues map[string]any
// EncodedStorage represents the storage key and value serialized
// to be placed in Ethereum state.
type EncodedStorage struct {
Key common.Hash
Value common.Hash
}
// EncodedStorage will encode a storage layout
func EncodeStorage(entry solc.StorageLayoutEntry, value any, storageType solc.StorageLayoutType) ([]*EncodedStorage, error) {
if storageType.NumberOfBytes > 32 {
return nil, fmt.Errorf("%s is larger than 32 bytes", entry.Label)
}
encoded, err := EncodeStorageKeyValue(value, entry, storageType)
if err != nil {
return nil, err
}
return encoded, nil
}
var errInvalidType = errors.New("invalid type")
var errUnimplemented = errors.New("type unimplemented")
// ComputeStorageSlots will compute the storage slots for a given contract.
func ComputeStorageSlots(layout *solc.StorageLayout, values StorageValues) ([]*EncodedStorage, error) {
encodedStorage := make([]*EncodedStorage, 0)
for label, value := range values {
var target solc.StorageLayoutEntry
for _, entry := range layout.Storage {
if label == entry.Label {
target = entry
}
}
if target.Label == "" {
return nil, fmt.Errorf("storage layout entry for %s not found", label)
}
storageType := layout.Types[target.Type]
if storageType.Label == "" {
return nil, fmt.Errorf("storage type for %s not found", label)
}
storage, err := EncodeStorage(target, value, storageType)
if err != nil {
return nil, fmt.Errorf("cannot encode storage: %w", err)
}
encodedStorage = append(encodedStorage, storage...)
}
results := MergeStorage(encodedStorage)
return results, nil
}
// MergeStorage will combine any overlapping storage slots for
// when values are tightly packed. Do this by checking to see if any
// of the produced storage slots have a matching key, if so use a
// binary or to add the storage values together
func MergeStorage(storage []*EncodedStorage) []*EncodedStorage {
encoded := make(map[common.Hash]common.Hash)
for _, storage := range storage {
if prev, ok := encoded[storage.Key]; ok {
combined := new(big.Int).Or(prev.Big(), storage.Value.Big())
encoded[storage.Key] = common.BigToHash(combined)
} else {
encoded[storage.Key] = storage.Value
}
}
results := make([]*EncodedStorage, 0)
for key, val := range encoded {
results = append(results, &EncodedStorage{key, val})
}
return results
}
package state_test
import (
"encoding/json"
"math/big"
"os"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum-optimism/optimism/state-surgery/solc"
"github.com/ethereum-optimism/optimism/state-surgery/state"
"github.com/ethereum-optimism/optimism/state-surgery/state/testdata"
"github.com/stretchr/testify/require"
)
var (
// layout is the storage layout used in tests
layout solc.StorageLayout
// testKey is the same test key that geth uses
testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
// chainID is the chain id used for simulated backends
chainID = big.NewInt(1337)
)
// Read the test data from disk asap
func init() {
data, err := os.ReadFile("./testdata/layout.json")
if err != nil {
panic("layout.json not found")
}
if err := json.Unmarshal(data, &layout); err != nil {
panic("cannot unmarshal storage layout")
}
}
func TestSetAndGetStorageSlots(t *testing.T) {
values := state.StorageValues{}
values["_uint256"] = new(big.Int).SetUint64(0xafff_ffff_ffff_ffff)
values["_address"] = common.HexToAddress("0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8")
values["_bool"] = true
values["offset0"] = uint8(0xaa)
values["offset1"] = uint8(0xbb)
values["offset2"] = uint16(0x0c0c)
values["offset3"] = uint32(0xf33d35)
values["offset4"] = uint64(0xd34dd34d00)
values["offset5"] = new(big.Int).SetUint64(0x43ad0043ad0043ad)
addresses := make(map[any]any)
addresses[big.NewInt(1)] = common.Address{19: 0xff}
values["addresses"] = addresses
slots, err := state.ComputeStorageSlots(&layout, values)
require.Nil(t, err)
backend := backends.NewSimulatedBackend(
core.GenesisAlloc{
crypto.PubkeyToAddress(testKey.PublicKey): {Balance: big.NewInt(10000000000000000)},
},
15000000,
)
opts, err := bind.NewKeyedTransactorWithChainID(testKey, chainID)
require.Nil(t, err)
_, _, contract, err := testdata.DeployTestdata(opts, backend)
require.Nil(t, err)
backend.Commit()
// Call each of the methods to make sure that they are set to their 0 values
testContractStateValuesAreEmpty(t, contract)
// Send transactions through the set storage API on the contract
for _, slot := range slots {
_, err := contract.SetStorage(opts, slot.Key, slot.Value)
require.Nil(t, err)
}
backend.Commit()
testContractStateValuesAreSet(t, contract, values)
// Call the get storage API on the contract to double check
// that the storage slots have been set correctly
for _, slot := range slots {
value, err := contract.GetStorage(&bind.CallOpts{}, slot.Key)
require.Nil(t, err)
require.Equal(t, value[:], slot.Value.Bytes())
}
}
// Ensure that all the storage variables are set after setting storage
// through the contract
func testContractStateValuesAreSet(t *testing.T, contract *testdata.Testdata, values state.StorageValues) {
OUTER:
for key, value := range values {
var res any
var err error
switch key {
case "_uint256":
res, err = contract.Uint256(&bind.CallOpts{})
case "_address":
res, err = contract.Address(&bind.CallOpts{})
case "_bool":
res, err = contract.Bool(&bind.CallOpts{})
case "offset0":
res, err = contract.Offset0(&bind.CallOpts{})
case "offset1":
res, err = contract.Offset1(&bind.CallOpts{})
case "offset2":
res, err = contract.Offset2(&bind.CallOpts{})
case "offset3":
res, err = contract.Offset3(&bind.CallOpts{})
case "offset4":
res, err = contract.Offset4(&bind.CallOpts{})
case "offset5":
res, err = contract.Offset5(&bind.CallOpts{})
case "addresses":
addrs, ok := value.(map[any]any)
require.Equal(t, ok, true)
for mapKey, mapVal := range addrs {
res, err = contract.Addresses(&bind.CallOpts{}, mapKey.(*big.Int))
require.Nil(t, err)
require.Equal(t, mapVal, res)
continue OUTER
}
default:
require.Fail(t, "Unknown variable label", key)
}
require.Nil(t, err)
require.Equal(t, res, value)
}
}
func testContractStateValuesAreEmpty(t *testing.T, contract *testdata.Testdata) {
addr, err := contract.Address(&bind.CallOpts{})
require.Nil(t, err)
require.Equal(t, addr, common.Address{})
boolean, err := contract.Bool(&bind.CallOpts{})
require.Nil(t, err)
require.Equal(t, boolean, false)
uint256, err := contract.Uint256(&bind.CallOpts{})
require.Nil(t, err)
require.Equal(t, uint256.Uint64(), uint64(0))
offset0, err := contract.Offset0(&bind.CallOpts{})
require.Nil(t, err)
require.Equal(t, offset0, uint8(0))
offset1, err := contract.Offset1(&bind.CallOpts{})
require.Nil(t, err)
require.Equal(t, offset1, uint8(0))
offset2, err := contract.Offset2(&bind.CallOpts{})
require.Nil(t, err)
require.Equal(t, offset2, uint16(0))
offset3, err := contract.Offset3(&bind.CallOpts{})
require.Nil(t, err)
require.Equal(t, offset3, uint32(0))
offset4, err := contract.Offset4(&bind.CallOpts{})
require.Nil(t, err)
require.Equal(t, offset4, uint64(0))
offset5, err := contract.Offset5(&bind.CallOpts{})
require.Nil(t, err)
require.Equal(t, offset5.Uint64(), uint64(0))
}
func TestMergeStorage(t *testing.T) {
cases := []struct {
input []*state.EncodedStorage
expect []*state.EncodedStorage
}{
{
// One input should be the same result
input: []*state.EncodedStorage{
{
Key: common.Hash{},
Value: common.Hash{},
},
},
expect: []*state.EncodedStorage{
{
Key: common.Hash{},
Value: common.Hash{},
},
},
},
{
// Two duplicate inputs should be merged
input: []*state.EncodedStorage{
{
Key: common.Hash{1},
Value: common.Hash{},
},
{
Key: common.Hash{1},
Value: common.Hash{},
},
},
expect: []*state.EncodedStorage{
{
Key: common.Hash{1},
Value: common.Hash{},
},
},
},
{
// Two different inputs should be the same result
input: []*state.EncodedStorage{
{
Key: common.Hash{1},
Value: common.Hash{},
},
{
Key: common.Hash{2},
Value: common.Hash{},
},
},
expect: []*state.EncodedStorage{
{
Key: common.Hash{1},
Value: common.Hash{},
},
{
Key: common.Hash{2},
Value: common.Hash{},
},
},
},
{
// Two matching keys should be merged bitwise
input: []*state.EncodedStorage{
{
Key: common.Hash{},
Value: common.Hash{0x00, 0x01},
},
{
Key: common.Hash{},
Value: common.Hash{0x02, 0x00},
},
},
expect: []*state.EncodedStorage{
{
Key: common.Hash{},
Value: common.Hash{0x02, 0x01},
},
},
},
}
for _, test := range cases {
got := state.MergeStorage(test.input)
require.Equal(t, got, test.expect)
}
}
func TestEncodeUintValue(t *testing.T) {
cases := []struct {
number any
offset uint
expect common.Hash
}{
{
number: 0,
offset: 0,
expect: common.Hash{},
},
{
number: big.NewInt(1),
offset: 0,
expect: common.Hash{31: 0x01},
},
{
number: uint64(2),
offset: 0,
expect: common.Hash{31: 0x02},
},
{
number: uint8(3),
offset: 0,
expect: common.Hash{31: 0x03},
},
{
number: uint16(4),
offset: 0,
expect: common.Hash{31: 0x04},
},
{
number: uint32(5),
offset: 0,
expect: common.Hash{31: 0x05},
},
{
number: int(6),
offset: 0,
expect: common.Hash{31: 0x06},
},
{
number: 1,
offset: 1,
expect: common.Hash{30: 0x01},
},
{
number: 1,
offset: 10,
expect: common.Hash{21: 0x01},
},
}
for _, test := range cases {
got, err := state.EncodeUintValue(test.number, test.offset)
require.Nil(t, err)
require.Equal(t, got, test.expect)
}
}
func TestEncodeBoolValue(t *testing.T) {
cases := []struct {
boolean any
offset uint
expect common.Hash
}{
{
boolean: true,
offset: 0,
expect: common.Hash{31: 0x01},
},
{
boolean: false,
offset: 0,
expect: common.Hash{},
},
{
boolean: true,
offset: 1,
expect: common.Hash{30: 0x01},
},
{
boolean: false,
offset: 1,
expect: common.Hash{},
},
{
boolean: "true",
offset: 0,
expect: common.Hash{31: 0x01},
},
{
boolean: "false",
offset: 0,
expect: common.Hash{},
},
}
for _, test := range cases {
got, err := state.EncodeBoolValue(test.boolean, test.offset)
require.Nil(t, err)
require.Equal(t, got, test.expect)
}
}
func TestEncodeAddressValue(t *testing.T) {
cases := []struct {
addr any
offset uint
expect common.Hash
}{
{
addr: common.Address{},
offset: 0,
expect: common.Hash{},
},
{
addr: common.Address{0x01},
offset: 0,
expect: common.Hash{12: 0x01},
},
{
addr: "0x829BD824B016326A401d083B33D092293333A830",
offset: 0,
expect: common.HexToHash("0x829BD824B016326A401d083B33D092293333A830"),
},
{
addr: common.Address{19: 0x01},
offset: 1,
expect: common.Hash{30: 0x01},
},
}
for _, test := range cases {
got, err := state.EncodeAddressValue(test.addr, test.offset)
require.Nil(t, err)
require.Equal(t, got, test.expect)
}
}
This diff is collapsed.
{
"storage": [
{
"astId": 3,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "_address",
"offset": 0,
"slot": "0",
"type": "t_address"
},
{
"astId": 7,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "addresses",
"offset": 0,
"slot": "1",
"type": "t_mapping(t_uint256,t_address)"
},
{
"astId": 9,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "_bool",
"offset": 0,
"slot": "2",
"type": "t_bool"
},
{
"astId": 11,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "_uint256",
"offset": 0,
"slot": "3",
"type": "t_uint256"
},
{
"astId": 13,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "offset0",
"offset": 0,
"slot": "4",
"type": "t_uint8"
},
{
"astId": 15,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "offset1",
"offset": 1,
"slot": "4",
"type": "t_uint8"
},
{
"astId": 17,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "offset2",
"offset": 2,
"slot": "4",
"type": "t_uint16"
},
{
"astId": 19,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "offset3",
"offset": 4,
"slot": "4",
"type": "t_uint32"
},
{
"astId": 21,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "offset4",
"offset": 8,
"slot": "4",
"type": "t_uint64"
},
{
"astId": 23,
"contract": "contracts/HelloWorld.sol:HelloWorld",
"label": "offset5",
"offset": 16,
"slot": "4",
"type": "t_uint128"
}
],
"types": {
"t_address": {
"encoding": "inplace",
"label": "address",
"numberOfBytes": "20"
},
"t_bool": {
"encoding": "inplace",
"label": "bool",
"numberOfBytes": "1"
},
"t_mapping(t_uint256,t_address)": {
"encoding": "mapping",
"key": "t_uint256",
"label": "mapping(uint256 => address)",
"numberOfBytes": "32",
"value": "t_address"
},
"t_uint128": {
"encoding": "inplace",
"label": "uint128",
"numberOfBytes": "16"
},
"t_uint16": {
"encoding": "inplace",
"label": "uint16",
"numberOfBytes": "2"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
},
"t_uint32": {
"encoding": "inplace",
"label": "uint32",
"numberOfBytes": "4"
},
"t_uint64": {
"encoding": "inplace",
"label": "uint64",
"numberOfBytes": "8"
},
"t_uint8": {
"encoding": "inplace",
"label": "uint8",
"numberOfBytes": "1"
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment