From f77c239296903e9fe5cbbf0266625b7b44e3c865 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 4 Jun 2020 17:16:32 +0300 Subject: [PATCH 1/5] mpt: fix extension node cache invalidation It should always be invalidated if something changes in the `next` (below the extension node). --- pkg/core/mpt/trie.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/mpt/trie.go b/pkg/core/mpt/trie.go index 3c38424c0..08d128d88 100644 --- a/pkg/core/mpt/trie.go +++ b/pkg/core/mpt/trie.go @@ -261,7 +261,6 @@ func (t *Trie) deleteFromExtension(n *ExtensionNode, path []byte) (Node, error) case *ExtensionNode: n.key = append(n.key, nxt.key...) n.next = nxt.next - n.invalidateCache() case *HashNode: if nxt.IsEmpty() { return nxt, nil @@ -269,6 +268,7 @@ func (t *Trie) deleteFromExtension(n *ExtensionNode, path []byte) (Node, error) default: n.next = r } + n.invalidateCache() return n, nil } From 685d3eb8706c6f3eedf87a291af0cc8a05983fbb Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 4 Jun 2020 17:18:15 +0300 Subject: [PATCH 2/5] dao: prevent double serialization of StorageItems Converting to MPT value serializes the StorageItem, so it makes no sense doing it again. --- pkg/core/dao/dao.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index b6fb6023b..a1865517c 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -475,7 +475,7 @@ func (dao *Simple) PutStorageItem(scripthash util.Uint160, key []byte, si *state if err := dao.MPT.Put(k, v); err != nil && err != mpt.ErrNotFound { return err } - return dao.Put(si, stKey) + return dao.Store.Put(stKey, v[1:]) } // DeleteStorageItem drops storage item for the given script with the From 69ccca675dafab78eca7208c96510b6f52d443cd Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 4 Jun 2020 17:19:30 +0300 Subject: [PATCH 3/5] core: fix PrevHash calculation for MPTRoot This was differing from C# notion of PrevHash. It's not a previous root, but rather a hash of the previous serialized MPTRoot structure (that is to be signed by CNs). --- pkg/core/blockchain.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 88bb71db9..f20646f11 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -835,7 +835,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { if err != nil { return errors.WithMessagef(err, "can't get previous state root") } - prevHash = prev.Root + prevHash = hash.DoubleSha256(prev.GetSignedPart()) } err := bc.AddStateRoot(&state.MPTRoot{ MPTRootBase: state.MPTRootBase{ @@ -1803,7 +1803,7 @@ func (bc *Blockchain) verifyStateRoot(r *state.MPTRoot) error { prev, err := bc.GetStateRoot(r.Index - 1) if err != nil { return errors.New("can't get previous state root") - } else if !prev.Root.Equals(r.PrevHash) { + } else if !r.PrevHash.Equals(hash.DoubleSha256(prev.GetSignedPart())) { return errors.New("previous hash mismatch") } else if prev.Version != r.Version { return errors.New("version mismatch") From e3af560d115f0d4ae3aee5b2f992926031d33250 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 4 Jun 2020 17:21:58 +0300 Subject: [PATCH 4/5] dao: optimize storage cache flushing Items were serialized several times if there were several successful transactions in a block, prevent that by using State field as a bitfield (as it almost was intended to) and adding one more bit. It also eliminates useless duplicate MPT traversions. Confirmed to not break storage changes up to 3.3M on testnet. --- pkg/core/dao/cacheddao.go | 11 +++++++---- pkg/core/dao/storage_item.go | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/core/dao/cacheddao.go b/pkg/core/dao/cacheddao.go index 0fe8eba62..fc905f919 100644 --- a/pkg/core/dao/cacheddao.go +++ b/pkg/core/dao/cacheddao.go @@ -245,6 +245,7 @@ func (cd *Cached) FlushStorage() error { return err } } + ti.State |= flushedState } } return nil @@ -275,7 +276,7 @@ func (cd *Cached) getStorageItemNoCache(scripthash util.Uint160, key []byte) *st func (cd *Cached) getStorageItemInt(scripthash util.Uint160, key []byte, putToCache bool) *state.StorageItem { ti := cd.storage.getItem(scripthash, key) if ti != nil { - if ti.State == delOp { + if ti.State&delOp != 0 { return nil } return copyItem(&ti.StorageItem) @@ -303,8 +304,10 @@ func (cd *Cached) PutStorageItem(scripthash util.Uint160, key []byte, si *state. item := copyItem(si) ti := cd.storage.getItem(scripthash, key) if ti != nil { - if ti.State == delOp || ti.State == getOp { + if ti.State&(delOp|getOp) != 0 { ti.State = putOp + } else { + ti.State = addOp } ti.StorageItem = *item return nil @@ -357,7 +360,7 @@ func (cd *Cached) GetStorageItemsIterator(hash util.Uint160, prefix []byte) (Sto for ; keyIndex < len(cd.storage.keys[hash]); keyIndex++ { k := cd.storage.keys[hash][keyIndex] v := cache[k] - if v.State != delOp && bytes.HasPrefix([]byte(k), prefix) { + if v.State&delOp == 0 && bytes.HasPrefix([]byte(k), prefix) { val := make([]byte, len(v.StorageItem.Value)) copy(val, v.StorageItem.Value) return []byte(k), val, nil @@ -404,7 +407,7 @@ func (cd *Cached) GetStorageItems(hash util.Uint160, prefix []byte) ([]StorageIt for _, k := range cd.storage.keys[hash] { v := cache[k] - if v.State != delOp { + if v.State&delOp == 0 { val := make([]byte, len(v.StorageItem.Value)) copy(val, v.StorageItem.Value) result = append(result, StorageItemWithKey{ diff --git a/pkg/core/dao/storage_item.go b/pkg/core/dao/storage_item.go index 5a961e6bc..ade05cfd6 100644 --- a/pkg/core/dao/storage_item.go +++ b/pkg/core/dao/storage_item.go @@ -24,6 +24,7 @@ const ( delOp addOp putOp + flushedState ) func newItemCache() *itemCache { From a1c4d7ce260cbc7c5edbc1fa0390c77cd87de288 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 4 Jun 2020 17:25:57 +0300 Subject: [PATCH 5/5] core: do MPT compaction every once in a while We need to compact our in-memory MPT from time to time, otherwise it quickly fills up all available memory. This raises two obvious quesions --- when to do that and to what level do that. As for 'when', I think it's quite easy to use our regular persistence interval as an anchor (and it also frees up some memory), but we can't do that in the persistence routine itself because of synchronization issues (adding some synchronization primitives would add some cost that I'd also like to avoid), so do it indirectly by comparing persisted and current height in `storeBlock`. Choosing proper level is another problem, but if we're to roughly estimate one full branch node to use 1K of memory (usually it's way less than that) then we can easily store 1K of these nodes and that gives us a depth of 10 for our trie. --- pkg/core/blockchain.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index f20646f11..4c6247faf 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -859,6 +859,12 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { return err } bc.dao.MPT.Flush() + // Every persist cycle we also compact our in-memory MPT. + persistedHeight := atomic.LoadUint32(&bc.persistedHeight) + if persistedHeight == block.Index-1 { + // 10 is good and roughly estimated to fit remaining trie into 1M of memory. + bc.dao.MPT.Collapse(10) + } bc.topBlock.Store(block) atomic.StoreUint32(&bc.blockHeight, block.Index) bc.memPool.RemoveStale(bc.isTxStillRelevant)