core/state: lazy-load account storage tries and code

With this change, StateObject can work without loading the storage trie
and contract code. This is required because we're about to cache state
objects and want to reduce the amount of memory referenced by idle
objects.
This commit is contained in:
Felix Lange 2016-09-23 00:10:00 +02:00
parent 9df74cb584
commit e6ed80e899
10 changed files with 186 additions and 174 deletions

View file

@ -135,11 +135,8 @@ func (b *SimulatedBackend) StorageAt(ctx context.Context, contract common.Addres
return nil, errBlockNumberUnsupported
}
statedb, _ := b.blockchain.State()
if obj := statedb.GetStateObject(contract); obj != nil {
val := obj.GetState(key)
return val[:], nil
}
return nil, nil
val := statedb.GetState(contract, key)
return val[:], nil
}
// TransactionReceipt returns the receipt of a transaction.

View file

@ -50,15 +50,15 @@ func (self *StateDB) RawDump() World {
if err != nil {
panic(err)
}
account := Account{
Balance: stateObject.balance.String(),
Nonce: stateObject.nonce,
Root: common.Bytes2Hex(stateObject.Root()),
CodeHash: common.Bytes2Hex(stateObject.codeHash),
Code: common.Bytes2Hex(stateObject.Code()),
Balance: stateObject.Balance().String(),
Nonce: stateObject.Nonce(),
Root: common.Bytes2Hex(stateObject.data.Root[:]),
CodeHash: common.Bytes2Hex(stateObject.CodeHash()),
Code: common.Bytes2Hex(stateObject.Code(self.db)),
Storage: make(map[string]string),
}
stateObject.initTrie(self.db)
storageIt := stateObject.trie.Iterator()
for storageIt.Next() {
account.Storage[common.Bytes2Hex(self.trie.GetKey(storageIt.Key))] = common.Bytes2Hex(storageIt.Value)
@ -76,12 +76,3 @@ func (self *StateDB) Dump() []byte {
return json
}
// Debug stuff
func (self *StateObject) CreateOutputForDiff() {
fmt.Printf("%x %x %x %x\n", self.Address(), self.Root(), self.balance.Bytes(), self.nonce)
it := self.trie.Iterator()
for it.Next() {
fmt.Printf("%x %x\n", it.Key, it.Value)
}
}

View file

@ -127,7 +127,7 @@ func (ms *ManagedState) getAccount(addr common.Address) *account {
// Always make sure the state account nonce isn't actually higher
// than the tracked one.
so := ms.StateDB.GetStateObject(addr)
if so != nil && uint64(len(account.nonces))+account.nstart < so.nonce {
if so != nil && uint64(len(account.nonces))+account.nstart < so.Nonce() {
ms.accounts[addr] = newAccount(so)
}
@ -137,5 +137,5 @@ func (ms *ManagedState) getAccount(addr common.Address) *account {
}
func newAccount(so *StateObject) *account {
return &account{so, so.nonce, nil}
return &account{so, so.Nonce(), nil}
}

View file

@ -29,7 +29,8 @@ func create() (*ManagedState, *account) {
db, _ := ethdb.NewMemDatabase()
statedb, _ := New(common.Hash{}, db)
ms := ManageState(statedb)
so := &StateObject{address: addr, nonce: 100}
so := &StateObject{address: addr}
so.SetNonce(100)
ms.StateDB.stateObjects[addr] = so
ms.accounts[addr] = newAccount(so)
@ -92,7 +93,7 @@ func TestRemoteNonceChange(t *testing.T) {
account.nonces = append(account.nonces, nn...)
nonce := ms.NewNonce(addr)
ms.StateDB.stateObjects[addr].nonce = 200
ms.StateDB.stateObjects[addr].data.Nonce = 200
nonce = ms.NewNonce(addr)
if nonce != 200 {
t.Error("expected nonce after remote update to be", 201, "got", nonce)
@ -100,7 +101,7 @@ func TestRemoteNonceChange(t *testing.T) {
ms.NewNonce(addr)
ms.NewNonce(addr)
ms.NewNonce(addr)
ms.StateDB.stateObjects[addr].nonce = 200
ms.StateDB.stateObjects[addr].data.Nonce = 200
nonce = ms.NewNonce(addr)
if nonce != 204 {
t.Error("expected nonce after remote update to be", 201, "got", nonce)

View file

@ -57,112 +57,160 @@ func (self Storage) Copy() Storage {
return cpy
}
// StateObject represents an Ethereum account which is being modified.
//
// 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 {
trie *trie.SecureTrie
address common.Address // Ethereum address of this account
data accountData
// Address belonging to this account
address common.Address
// The balance of the account
balance *big.Int
// The nonce of the account
nonce uint64
// The code hash if code is present (i.e. a contract)
codeHash []byte
// The code for this account
code Code
// Cached storage (flushed when updated)
storage Storage
// 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
// Mark for deletion
// Write caches.
trie *trie.SecureTrie // storage trie, which becomes non-nil on first access
code Code // contract bytecode, which gets set when code is loaded
storage Storage // Cached storage (flushed when updated)
// Cache flags.
// When an object is marked for deletion it will be delete from the trie
// during the "update" phase of the state transition
dirty bool // true if anything has changed
remove bool
deleted bool
dirty bool
}
// accountData is the Ethereum consensus representation of accounts.
// These objects are stored in the main account trie.
type accountData struct {
Nonce uint64
Balance *big.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
}
func NewStateObject(address common.Address, db trie.Database) *StateObject {
object := &StateObject{
address: address,
balance: new(big.Int),
dirty: true,
codeHash: emptyCodeHash,
storage: make(Storage),
address: address,
data: accountData{Balance: new(big.Int), CodeHash: emptyCodeHash},
storage: make(Storage),
dirty: true,
}
object.trie, _ = trie.NewSecure(common.Hash{}, db)
return object
}
// setError remembers the first non-nil error it is called with.
func (self *StateObject) setError(err error) {
if self.dbErr == nil {
self.dbErr = err
}
}
func (self *StateObject) MarkForDeletion() {
self.remove = true
self.dirty = true
if glog.V(logger.Core) {
glog.Infof("%x: #%d %v X\n", self.Address(), self.nonce, self.balance)
glog.Infof("%x: #%d %v X\n", self.Address(), self.Nonce(), self.Balance())
}
}
func (c *StateObject) getAddr(addr common.Hash) common.Hash {
var ret []byte
rlp.DecodeBytes(c.trie.Get(addr[:]), &ret)
return common.BytesToHash(ret)
}
func (c *StateObject) setAddr(addr, value common.Hash) {
v, err := rlp.EncodeToBytes(bytes.TrimLeft(value[:], "\x00"))
if err != nil {
// if RLPing failed we better panic and not fail silently. This would be considered a consensus issue
panic(err)
}
c.trie.Update(addr[:], v)
}
func (self *StateObject) GetState(key common.Hash) common.Hash {
value, exists := self.storage[key]
if !exists {
value = self.getAddr(key)
if (value != common.Hash{}) {
self.storage[key] = value
func (c *StateObject) initTrie(db trie.Database) {
if c.trie == nil {
var err error
c.trie, err = trie.NewSecure(c.data.Root, db)
if err != nil {
c.trie, _ = trie.NewSecure(common.Hash{}, db)
c.setError(fmt.Errorf("can't create storage trie: %v", err))
}
}
}
// GetState returns a value in account storage.
func (self *StateObject) GetState(db trie.Database, key common.Hash) common.Hash {
value, exists := self.storage[key]
if exists {
return value
}
// Load from DB in case it is missing.
self.initTrie(db)
var ret []byte
rlp.DecodeBytes(self.trie.Get(key[:]), &ret)
value = common.BytesToHash(ret)
if (value != common.Hash{}) {
self.storage[key] = value
}
return value
}
// SetState updates a value in account storage.
func (self *StateObject) SetState(key, value common.Hash) {
self.storage[key] = value
self.dirty = true
}
// Update updates the current cached storage to the trie
func (self *StateObject) Update() {
// updateTrie writes cached storage modifications into the object's storage trie.
func (self *StateObject) updateTrie(db trie.Database) {
self.initTrie(db)
for key, value := range self.storage {
if (value == common.Hash{}) {
self.trie.Delete(key[:])
continue
}
self.setAddr(key, value)
// Encoding []byte cannot fail, ok to ignore the error.
v, _ := rlp.EncodeToBytes(bytes.TrimLeft(value[:], "\x00"))
self.trie.Update(key[:], v)
}
}
// UpdateRoot sets the trie root to the current root hash of
func (self *StateObject) UpdateRoot(db trie.Database) {
self.updateTrie(db)
self.data.Root = self.trie.Hash()
}
// CommitTrie the storage trie of the object to dwb.
// This updates the trie root.
func (self *StateObject) CommitTrie(db trie.Database, dbw trie.DatabaseWriter) error {
self.updateTrie(db)
if self.dbErr != nil {
fmt.Println("dbErr:", self.dbErr)
return self.dbErr
}
self.dbErr = nil
root, err := self.trie.CommitTo(dbw)
if err == nil {
self.data.Root = root
}
return err
}
func (c *StateObject) AddBalance(amount *big.Int) {
c.SetBalance(new(big.Int).Add(c.balance, amount))
c.SetBalance(new(big.Int).Add(c.Balance(), amount))
if glog.V(logger.Core) {
glog.Infof("%x: #%d %v (+ %v)\n", c.Address(), c.nonce, c.balance, amount)
glog.Infof("%x: #%d %v (+ %v)\n", c.Address(), c.Nonce(), c.Balance(), amount)
}
}
func (c *StateObject) SubBalance(amount *big.Int) {
c.SetBalance(new(big.Int).Sub(c.balance, amount))
c.SetBalance(new(big.Int).Sub(c.Balance(), amount))
if glog.V(logger.Core) {
glog.Infof("%x: #%d %v (- %v)\n", c.Address(), c.nonce, c.balance, amount)
glog.Infof("%x: #%d %v (- %v)\n", c.Address(), c.Nonce(), c.Balance(), amount)
}
}
func (c *StateObject) SetBalance(amount *big.Int) {
c.balance = amount
c.data.Balance = amount
c.dirty = true
}
@ -171,9 +219,8 @@ func (c *StateObject) ReturnGas(gas, price *big.Int) {}
func (self *StateObject) Copy(db trie.Database) *StateObject {
stateObject := NewStateObject(self.address, db)
stateObject.balance.Set(self.balance)
stateObject.codeHash = common.CopyBytes(self.codeHash)
stateObject.nonce = self.nonce
stateObject.data = self.data
stateObject.data.Balance.Set(self.data.Balance)
stateObject.trie = self.trie
stateObject.code = self.code
stateObject.storage = self.storage.Copy()
@ -188,40 +235,48 @@ func (self *StateObject) Copy(db trie.Database) *StateObject {
// Attribute accessors
//
func (self *StateObject) Balance() *big.Int {
return self.balance
}
// Returns the address of the contract/account
func (c *StateObject) Address() common.Address {
return c.address
}
func (self *StateObject) Trie() *trie.SecureTrie {
return self.trie
}
func (self *StateObject) Root() []byte {
return self.trie.Root()
}
func (self *StateObject) Code() []byte {
return self.code
// LoadCode returns the contract code associated with this object, if any.
func (self *StateObject) Code(db trie.Database) []byte {
if self.code != nil {
return self.code
}
if bytes.Equal(self.CodeHash(), emptyCodeHash) {
return nil
}
code, err := db.Get(self.CodeHash())
if err != nil {
self.setError(fmt.Errorf("can't load code hash %x: %v", self.CodeHash(), err))
}
self.code = code
return code
}
func (self *StateObject) SetCode(code []byte) {
self.code = code
self.codeHash = crypto.Keccak256(code)
self.data.CodeHash = crypto.Keccak256(code)
self.dirty = true
}
func (self *StateObject) SetNonce(nonce uint64) {
self.nonce = nonce
self.data.Nonce = nonce
self.dirty = true
}
func (self *StateObject) CodeHash() []byte {
return self.data.CodeHash
}
func (self *StateObject) Balance() *big.Int {
return self.data.Balance
}
func (self *StateObject) Nonce() uint64 {
return self.nonce
return self.data.Nonce
}
// Never called, but must be present to allow StateObject to be used
@ -247,38 +302,14 @@ func (self *StateObject) ForEachStorage(cb func(key, value common.Hash) bool) {
}
}
type extStateObject struct {
Nonce uint64
Balance *big.Int
Root common.Hash
CodeHash []byte
}
// EncodeRLP implements rlp.Encoder.
func (c *StateObject) EncodeRLP(w io.Writer) error {
return rlp.Encode(w, []interface{}{c.nonce, c.balance, c.Root(), c.codeHash})
return rlp.Encode(w, c.data)
}
// DecodeObject decodes an RLP-encoded state object.
func DecodeObject(address common.Address, db trie.Database, data []byte) (*StateObject, error) {
var (
obj = &StateObject{address: address, storage: make(Storage)}
ext extStateObject
err error
)
if err = rlp.DecodeBytes(data, &ext); err != nil {
return nil, err
}
if obj.trie, err = trie.NewSecure(ext.Root, db); err != nil {
return nil, err
}
if !bytes.Equal(ext.CodeHash, emptyCodeHash) {
if obj.code, err = db.Get(ext.CodeHash); err != nil {
return nil, fmt.Errorf("can't get code for hash %x: %v", ext.CodeHash, err)
}
}
obj.nonce = ext.Nonce
obj.balance = ext.Balance
obj.codeHash = ext.CodeHash
return obj, nil
obj := StateObject{address: address, storage: make(Storage)}
err := rlp.DecodeBytes(data, &obj.data)
return &obj, err
}

View file

@ -146,8 +146,8 @@ func TestSnapshot2(t *testing.T) {
// db, trie are already non-empty values
so0 := state.GetStateObject(stateobjaddr0)
so0.balance = big.NewInt(42)
so0.nonce = 43
so0.SetBalance(big.NewInt(42))
so0.SetNonce(43)
so0.SetCode([]byte{'c', 'a', 'f', 'e'})
so0.remove = false
so0.deleted = false
@ -157,8 +157,8 @@ func TestSnapshot2(t *testing.T) {
// and one with deleted == true
so1 := state.GetStateObject(stateobjaddr1)
so1.balance = big.NewInt(52)
so1.nonce = 53
so1.SetBalance(big.NewInt(52))
so1.SetNonce(53)
so1.SetCode([]byte{'c', 'a', 'f', 'e', '2'})
so1.remove = true
so1.deleted = true
@ -174,41 +174,50 @@ func TestSnapshot2(t *testing.T) {
state.Set(snapshot)
so0Restored := state.GetStateObject(stateobjaddr0)
so0Restored.GetState(storageaddr)
so1Restored := state.GetStateObject(stateobjaddr1)
// Update lazily-loaded values before comparing.
so0Restored.GetState(db, storageaddr)
so0Restored.Code(db)
// non-deleted is equal (restored)
compareStateObjects(so0Restored, so0, t)
// deleted should be nil, both before and after restore of state copy
so1Restored := state.GetStateObject(stateobjaddr1)
if so1Restored != nil {
t.Fatalf("deleted object not nil after restoring snapshot")
}
}
func compareStateObjects(so0, so1 *StateObject, t *testing.T) {
if so0.address != so1.address {
if so0.Address() != so1.Address() {
t.Fatalf("Address mismatch: have %v, want %v", so0.address, so1.address)
}
if so0.balance.Cmp(so1.balance) != 0 {
t.Fatalf("Balance mismatch: have %v, want %v", so0.balance, so1.balance)
if so0.Balance().Cmp(so1.Balance()) != 0 {
t.Fatalf("Balance mismatch: have %v, want %v", so0.Balance(), so1.Balance())
}
if so0.nonce != so1.nonce {
t.Fatalf("Nonce mismatch: have %v, want %v", so0.nonce, so1.nonce)
if so0.Nonce() != so1.Nonce() {
t.Fatalf("Nonce mismatch: have %v, want %v", so0.Nonce(), so1.Nonce())
}
if !bytes.Equal(so0.codeHash, so1.codeHash) {
t.Fatalf("CodeHash mismatch: have %v, want %v", so0.codeHash, so1.codeHash)
if so0.data.Root != so1.data.Root {
t.Errorf("Root mismatch: have %x, want %x", so0.data.Root[:], so1.data.Root[:])
}
if !bytes.Equal(so0.CodeHash(), so1.CodeHash()) {
t.Fatalf("CodeHash mismatch: have %v, want %v", so0.CodeHash(), so1.CodeHash())
}
if !bytes.Equal(so0.code, so1.code) {
t.Fatalf("Code mismatch: have %v, want %v", so0.code, so1.code)
}
if len(so1.storage) != len(so0.storage) {
t.Errorf("Storage size mismatch: have %d, want %d", len(so1.storage), len(so0.storage))
}
for k, v := range so1.storage {
if so0.storage[k] != v {
t.Fatalf("Storage key %s mismatch: have %v, want %v", k, so0.storage[k], v)
t.Errorf("Storage key %x mismatch: have %v, want %v", k, so0.storage[k], v)
}
}
for k, v := range so0.storage {
if so1.storage[k] != v {
t.Fatalf("Storage key %s mismatch: have %v, want none.", k, v)
t.Errorf("Storage key %x mismatch: have %v, want none.", k, v)
}
}

View file

@ -137,7 +137,7 @@ func (self *StateDB) GetAccount(addr common.Address) vm.Account {
func (self *StateDB) GetBalance(addr common.Address) *big.Int {
stateObject := self.GetStateObject(addr)
if stateObject != nil {
return stateObject.balance
return stateObject.Balance()
}
return common.Big0
@ -146,7 +146,7 @@ func (self *StateDB) GetBalance(addr common.Address) *big.Int {
func (self *StateDB) GetNonce(addr common.Address) uint64 {
stateObject := self.GetStateObject(addr)
if stateObject != nil {
return stateObject.nonce
return stateObject.Nonce()
}
return StartingNonce
@ -155,18 +155,16 @@ func (self *StateDB) GetNonce(addr common.Address) uint64 {
func (self *StateDB) GetCode(addr common.Address) []byte {
stateObject := self.GetStateObject(addr)
if stateObject != nil {
return stateObject.code
return stateObject.Code(self.db)
}
return nil
}
func (self *StateDB) GetState(a common.Address, b common.Hash) common.Hash {
stateObject := self.GetStateObject(a)
if stateObject != nil {
return stateObject.GetState(b)
return stateObject.GetState(self.db, b)
}
return common.Hash{}
}
@ -214,8 +212,7 @@ func (self *StateDB) Delete(addr common.Address) bool {
stateObject := self.GetStateObject(addr)
if stateObject != nil {
stateObject.MarkForDeletion()
stateObject.balance = new(big.Int)
stateObject.data.Balance = new(big.Int)
return true
}
@ -245,7 +242,7 @@ func (self *StateDB) DeleteStateObject(stateObject *StateObject) {
//delete(self.stateObjects, addr)
}
// Retrieve a state object given my the address. Nil if not found
// Retrieve a state object given my the address. Returns nil if not found.
func (self *StateDB) GetStateObject(addr common.Address) (stateObject *StateObject) {
stateObject = self.stateObjects[addr]
if stateObject != nil {
@ -305,7 +302,7 @@ func (self *StateDB) CreateStateObject(addr common.Address) *StateObject {
// If it existed set the balance to the new account
if so != nil {
newSo.balance = so.balance
newSo.data.Balance = so.data.Balance
}
return newSo
@ -363,7 +360,7 @@ func (s *StateDB) IntermediateRoot() common.Hash {
if stateObject.remove {
s.DeleteStateObject(stateObject)
} else {
stateObject.Update()
stateObject.UpdateRoot(s.db)
s.UpdateStateObject(stateObject)
}
}
@ -407,7 +404,7 @@ func (s *StateDB) CommitBatch() (root common.Hash, batch ethdb.Batch) {
return root, batch
}
func (s *StateDB) commit(db trie.DatabaseWriter) (common.Hash, error) {
func (s *StateDB) commit(dbw trie.DatabaseWriter) (common.Hash, error) {
s.refund = new(big.Int)
for _, stateObject := range s.stateObjects {
@ -417,36 +414,24 @@ func (s *StateDB) commit(db trie.DatabaseWriter) (common.Hash, error) {
s.DeleteStateObject(stateObject)
} else {
// Write any contract code associated with the state object
if len(stateObject.code) > 0 {
if err := db.Put(stateObject.codeHash, stateObject.code); err != nil {
if stateObject.code != nil {
if err := dbw.Put(stateObject.CodeHash(), stateObject.code); err != nil {
return common.Hash{}, err
}
}
// Write any storage changes in the state object to its trie.
stateObject.Update()
// Commit the trie of the object to the batch.
// This updates the trie root internally, so
// getting the root hash of the storage trie
// through UpdateStateObject is fast.
if _, err := stateObject.trie.CommitTo(db); err != nil {
// Write any storage changes in the state object to its storage trie.
if err := stateObject.CommitTrie(s.db, dbw); err != nil {
return common.Hash{}, err
}
// Update the object in the account trie.
// Update the object in the main account trie.
s.UpdateStateObject(stateObject)
}
stateObject.dirty = false
}
return s.trie.CommitTo(db)
return s.trie.CommitTo(dbw)
}
func (self *StateDB) Refunds() *big.Int {
return self.refund
}
// Debug stuff
func (self *StateDB) CreateOutputForDiff() {
for _, stateObject := range self.stateObjects {
stateObject.CreateOutputForDiff()
}
}

View file

@ -62,7 +62,7 @@ func makeTestState() (common.Hash, ethdb.Database) {
}
so.AddBalance(big.NewInt(int64(i)))
so.SetCode([]byte{i, i, i})
so.Update()
so.UpdateRoot(sdb)
st.UpdateStateObject(so)
}
root, _ := st.Commit()

View file

@ -187,7 +187,7 @@ func runStateTest(ruleSet RuleSet, test VmTest) error {
}
for addr, value := range account.Storage {
v := obj.GetState(common.HexToHash(addr))
v := statedb.GetState(obj.Address(), common.HexToHash(addr))
vexp := common.HexToHash(value)
if v != vexp {

View file

@ -205,11 +205,9 @@ func runVmTest(test VmTest) error {
if obj == nil {
continue
}
for addr, value := range account.Storage {
v := obj.GetState(common.HexToHash(addr))
v := statedb.GetState(obj.Address(), common.HexToHash(addr))
vexp := common.HexToHash(value)
if v != vexp {
return fmt.Errorf("(%x: %s) storage failed. Expected %x, got %x (%v %v)\n", obj.Address().Bytes()[0:4], addr, vexp, v, vexp.Big(), v.Big())
}