core,dao: implement Block-level storage caching

The order in which storage.Find items are returns depends on what items
were processed in previous transactions of the same block.
The easiest way to implement this sort of caching is to cache operations
with storage, flushing the only in `Persist()`.
This commit is contained in:
Evgenii Stratonikov 2020-05-06 14:34:17 +03:00
parent f0abbfd399
commit b96fe8173c
6 changed files with 246 additions and 20 deletions

View file

@ -1147,7 +1147,16 @@ func (bc *Blockchain) GetStorageItem(scripthash util.Uint160, key []byte) *state
// GetStorageItems returns all storage items for a given scripthash. // GetStorageItems returns all storage items for a given scripthash.
func (bc *Blockchain) GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) { func (bc *Blockchain) GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) {
return bc.dao.GetStorageItems(hash) siMap, err := bc.dao.GetStorageItems(hash)
if err != nil {
return nil, err
}
m := make(map[string]*state.StorageItem)
for i := range siMap {
val := siMap[i].StorageItem
m[string(siMap[i].Key)] = &val
}
return m, nil
} }
// GetBlock returns a Block by the given hash. // GetBlock returns a Block by the given hash.

View file

@ -1,7 +1,9 @@
package dao package dao
import ( import (
"bytes"
"errors" "errors"
"sort"
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
@ -18,6 +20,7 @@ type Cached struct {
unspents map[util.Uint256]*state.UnspentCoin unspents map[util.Uint256]*state.UnspentCoin
balances map[util.Uint160]*state.NEP5Balances balances map[util.Uint160]*state.NEP5Balances
transfers map[util.Uint160]map[uint32]*state.NEP5TransferLog transfers map[util.Uint160]map[uint32]*state.NEP5TransferLog
storage *itemCache
} }
// NewCached returns new Cached wrapping around given backing store. // NewCached returns new Cached wrapping around given backing store.
@ -27,7 +30,16 @@ func NewCached(d DAO) *Cached {
unspents := make(map[util.Uint256]*state.UnspentCoin) unspents := make(map[util.Uint256]*state.UnspentCoin)
balances := make(map[util.Uint160]*state.NEP5Balances) balances := make(map[util.Uint160]*state.NEP5Balances)
transfers := make(map[util.Uint160]map[uint32]*state.NEP5TransferLog) transfers := make(map[util.Uint160]map[uint32]*state.NEP5TransferLog)
return &Cached{d.GetWrapped(), accs, ctrs, unspents, balances, transfers} st := newItemCache()
dao := d.GetWrapped()
if cd, ok := dao.(*Cached); ok {
for h, m := range cd.storage.st {
for _, k := range cd.storage.keys[h] {
st.put(h, []byte(k), m[k].State, copyItem(&m[k].StorageItem))
}
}
}
return &Cached{dao, accs, ctrs, unspents, balances, transfers, st}
} }
// GetAccountStateOrNew retrieves Account from cache or underlying store // GetAccountStateOrNew retrieves Account from cache or underlying store
@ -140,6 +152,10 @@ func (cd *Cached) AppendNEP5Transfer(acc util.Uint160, index uint32, tr *state.N
// Persist flushes all the changes made into the (supposedly) persistent // Persist flushes all the changes made into the (supposedly) persistent
// underlying store. // underlying store.
func (cd *Cached) Persist() (int, error) { func (cd *Cached) Persist() (int, error) {
if err := cd.FlushStorage(); err != nil {
return 0, err
}
lowerCache, ok := cd.DAO.(*Cached) lowerCache, ok := cd.DAO.(*Cached)
// If the lower DAO is Cached, we only need to flush the MemCached DB. // If the lower DAO is Cached, we only need to flush the MemCached DB.
// This actually breaks DAO interface incapsulation, but for our current // This actually breaks DAO interface incapsulation, but for our current
@ -148,6 +164,9 @@ func (cd *Cached) Persist() (int, error) {
if ok { if ok {
var simpleCache *Simple var simpleCache *Simple
for simpleCache == nil { for simpleCache == nil {
if err := lowerCache.FlushStorage(); err != nil {
return 0, err
}
simpleCache, ok = lowerCache.DAO.(*Simple) simpleCache, ok = lowerCache.DAO.(*Simple)
if !ok { if !ok {
lowerCache, ok = cd.DAO.(*Cached) lowerCache, ok = cd.DAO.(*Cached)
@ -200,5 +219,137 @@ func (cd *Cached) GetWrapped() DAO {
cd.unspents, cd.unspents,
cd.balances, cd.balances,
cd.transfers, cd.transfers,
cd.storage,
} }
} }
// FlushStorage flushes storage changes to the underlying DAO.
func (cd *Cached) FlushStorage() error {
if d, ok := cd.DAO.(*Cached); ok {
d.storage.st = cd.storage.st
d.storage.keys = cd.storage.keys
return nil
}
for h, items := range cd.storage.st {
for _, k := range cd.storage.keys[h] {
ti := items[k]
switch ti.State {
case putOp, addOp:
err := cd.DAO.PutStorageItem(h, []byte(k), &ti.StorageItem)
if err != nil {
return err
}
case delOp:
err := cd.DAO.DeleteStorageItem(h, []byte(k))
if err != nil {
return err
}
}
}
}
return nil
}
func copyItem(si *state.StorageItem) *state.StorageItem {
val := make([]byte, len(si.Value))
copy(val, si.Value)
return &state.StorageItem{
Value: val,
IsConst: si.IsConst,
}
}
// GetStorageItem returns StorageItem if it exists in the given store.
func (cd *Cached) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem {
ti := cd.storage.getItem(scripthash, key)
if ti != nil {
if ti.State == delOp {
return nil
}
return copyItem(&ti.StorageItem)
}
si := cd.DAO.GetStorageItem(scripthash, key)
if si != nil {
cd.storage.put(scripthash, key, getOp, si)
return copyItem(si)
}
return nil
}
// PutStorageItem puts given StorageItem for given script with given
// key into the given store.
func (cd *Cached) PutStorageItem(scripthash util.Uint160, key []byte, si *state.StorageItem) error {
item := copyItem(si)
ti := cd.storage.getItem(scripthash, key)
if ti != nil {
if ti.State == delOp || ti.State == getOp {
ti.State = putOp
}
ti.StorageItem = *item
return nil
}
op := addOp
if it := cd.DAO.GetStorageItem(scripthash, key); it != nil {
op = putOp
}
cd.storage.put(scripthash, key, op, item)
return nil
}
// DeleteStorageItem drops storage item for the given script with the
// given key from the store.
func (cd *Cached) DeleteStorageItem(scripthash util.Uint160, key []byte) error {
ti := cd.storage.getItem(scripthash, key)
if ti != nil {
ti.State = delOp
ti.Value = nil
return nil
}
it := cd.DAO.GetStorageItem(scripthash, key)
if it != nil {
cd.storage.put(scripthash, key, delOp, it)
}
return nil
}
// GetStorageItems returns all storage items for a given scripthash.
func (cd *Cached) GetStorageItems(hash util.Uint160) ([]StorageItemWithKey, error) {
items, err := cd.DAO.GetStorageItems(hash)
if err != nil {
return nil, err
}
cache := cd.storage.getItems(hash)
if len(cache) == 0 {
return items, nil
}
result := make([]StorageItemWithKey, 0, len(items))
for i := range items {
_, ok := cache[string(items[i].Key)]
if !ok {
result = append(result, items[i])
}
}
sort.Slice(result, func(i, j int) bool { return bytes.Compare(result[i].Key, result[j].Key) == -1 })
for _, k := range cd.storage.keys[hash] {
v := cache[k]
if v.State != delOp {
val := make([]byte, len(v.StorageItem.Value))
copy(val, v.StorageItem.Value)
result = append(result, StorageItemWithKey{
StorageItem: state.StorageItem{
Value: val,
IsConst: v.StorageItem.IsConst,
},
Key: []byte(k),
})
}
}
return result, nil
}

View file

@ -35,7 +35,7 @@ type DAO interface {
GetNEP5Balances(acc util.Uint160) (*state.NEP5Balances, error) GetNEP5Balances(acc util.Uint160) (*state.NEP5Balances, error)
GetNEP5TransferLog(acc util.Uint160, index uint32) (*state.NEP5TransferLog, error) GetNEP5TransferLog(acc util.Uint160, index uint32) (*state.NEP5TransferLog, error)
GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem
GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) GetStorageItems(hash util.Uint160) ([]StorageItemWithKey, error)
GetTransaction(hash util.Uint256) (*transaction.Transaction, uint32, error) GetTransaction(hash util.Uint256) (*transaction.Transaction, uint32, error)
GetUnspentCoinState(hash util.Uint256) (*state.UnspentCoin, error) GetUnspentCoinState(hash util.Uint256) (*state.UnspentCoin, error)
GetValidatorState(publicKey *keys.PublicKey) (*state.Validator, error) GetValidatorState(publicKey *keys.PublicKey) (*state.Validator, error)
@ -435,9 +435,15 @@ func (dao *Simple) DeleteStorageItem(scripthash util.Uint160, key []byte) error
return dao.Store.Delete(makeStorageItemKey(scripthash, key)) return dao.Store.Delete(makeStorageItemKey(scripthash, key))
} }
// StorageItemWithKey is a Key-Value pair together with possible const modifier.
type StorageItemWithKey struct {
state.StorageItem
Key []byte
}
// GetStorageItems returns all storage items for a given scripthash. // GetStorageItems returns all storage items for a given scripthash.
func (dao *Simple) GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) { func (dao *Simple) GetStorageItems(hash util.Uint160) ([]StorageItemWithKey, error) {
var siMap = make(map[string]*state.StorageItem) var res []StorageItemWithKey
var err error var err error
saveToMap := func(k, v []byte) { saveToMap := func(k, v []byte) {
@ -445,21 +451,22 @@ func (dao *Simple) GetStorageItems(hash util.Uint160) (map[string]*state.Storage
return return
} }
r := io.NewBinReaderFromBuf(v) r := io.NewBinReaderFromBuf(v)
si := &state.StorageItem{} var s StorageItemWithKey
si.DecodeBinary(r) s.StorageItem.DecodeBinary(r)
if r.Err != nil { if r.Err != nil {
err = r.Err err = r.Err
return return
} }
// Cut prefix and hash. // Cut prefix and hash.
siMap[string(k[21:])] = si s.Key = k[21:]
res = append(res, s)
} }
dao.Store.Seek(storage.AppendPrefix(storage.STStorage, hash.BytesLE()), saveToMap) dao.Store.Seek(storage.AppendPrefix(storage.STStorage, hash.BytesLE()), saveToMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return siMap, nil return res, nil
} }
// makeStorageItemKey returns a key used to store StorageItem in the DB. // makeStorageItemKey returns a key used to store StorageItem in the DB.

View file

@ -0,0 +1,57 @@
package dao
import (
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/util"
)
type (
itemState int
trackedItem struct {
state.StorageItem
State itemState
}
itemCache struct {
st map[util.Uint160]map[string]*trackedItem
keys map[util.Uint160][]string
}
)
const (
getOp itemState = 1 << iota
delOp
addOp
putOp
)
func newItemCache() *itemCache {
return &itemCache{
make(map[util.Uint160]map[string]*trackedItem),
make(map[util.Uint160][]string),
}
}
func (c *itemCache) put(h util.Uint160, key []byte, op itemState, item *state.StorageItem) {
m := c.getItems(h)
m[string(key)] = &trackedItem{
StorageItem: *item,
State: op,
}
c.keys[h] = append(c.keys[h], string(key))
c.st[h] = m
}
func (c *itemCache) getItem(h util.Uint160, key []byte) *trackedItem {
m := c.getItems(h)
return m[string(key)]
}
func (c *itemCache) getItems(h util.Uint160) map[string]*trackedItem {
m, ok := c.st[h]
if !ok {
return make(map[string]*trackedItem)
}
return m
}

View file

@ -1,10 +1,10 @@
package core package core
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"math" "math"
"strings"
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -444,17 +444,18 @@ func (ic *interopContext) storageFind(v *vm.VM) error {
if err != nil { if err != nil {
return err return err
} }
prefix := string(v.Estack().Pop().Bytes()) pref := v.Estack().Pop().Bytes()
siMap, err := ic.dao.GetStorageItems(stc.ScriptHash) siMap, err := ic.dao.GetStorageItems(stc.ScriptHash)
if err != nil { if err != nil {
return err return err
} }
filteredMap := vm.NewMapItem() filteredMap := vm.NewMapItem()
for k, v := range siMap { for i := range siMap {
if strings.HasPrefix(k, prefix) { k := siMap[i].Key
filteredMap.Add(vm.NewByteArrayItem([]byte(k)), if bytes.HasPrefix(k, pref) {
vm.NewByteArrayItem(v.Value)) filteredMap.Add(vm.NewByteArrayItem(siMap[i].Key),
vm.NewByteArrayItem(siMap[i].Value))
} }
} }
@ -578,9 +579,10 @@ func (ic *interopContext) contractMigrate(v *vm.VM) error {
if err != nil { if err != nil {
return err return err
} }
for k, v := range siMap { for i := range siMap {
v.IsConst = false v := siMap[i].StorageItem
err = ic.dao.PutStorageItem(contract.ScriptHash(), []byte(k), v) siMap[i].IsConst = false
err = ic.dao.PutStorageItem(contract.ScriptHash(), siMap[i].Key, &v)
if err != nil { if err != nil {
return err return err
} }

View file

@ -561,8 +561,8 @@ func (ic *interopContext) contractDestroy(v *vm.VM) error {
if err != nil { if err != nil {
return err return err
} }
for k := range siMap { for i := range siMap {
_ = ic.dao.DeleteStorageItem(hash, []byte(k)) _ = ic.dao.DeleteStorageItem(hash, siMap[i].Key)
} }
} }
return nil return nil