From 17e7433725d63d6ffff813df6bd6882d59082ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Thu, 10 Jan 2019 10:51:49 +0200 Subject: [PATCH] core, trie: prevent genesis and last snapshot from being pruned --- core/blockchain.go | 36 ++++++++++++++++++++++++++---------- trie/database.go | 27 +++++++++++++++++++++++++-- trie/database_pruning.go | 18 ++++++++++++------ 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index b4f39d87fe..8da18f8853 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -95,9 +95,11 @@ type BlockChain struct { chainConfig *params.ChainConfig // Chain & network configuration cacheConfig *CacheConfig // Cache configuration for pruning - db ethdb.Database // Low level persistent database to store final content in - triegc *prque.Prque // Priority queue mapping block numbers to tries to gc - gcproc time.Duration // Accumulates canonical block processing for trie dumping + db ethdb.Database // Low level persistent database to store final content in + + gcqueue *prque.Prque // Priority queue mapping block numbers to tries to gc + gcsave common.Hash // Root hash of the last trie committed to disk + gcproc time.Duration // Accumulates canonical block processing for trie dumping hc *HeaderChain rmLogsFeed event.Feed @@ -159,7 +161,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par chainConfig: chainConfig, cacheConfig: cacheConfig, db: db, - triegc: prque.New(nil), + gcqueue: prque.New(nil), stateCache: state.NewDatabaseWithCache(db, cacheConfig.TrieCleanLimit), quit: make(chan struct{}), shouldPreserve: shouldPreserve, @@ -200,6 +202,12 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par } } } + // Forbid the genesis state (forever) and latest state (temporarilly) from being pruned + bc.stateCache.TrieDB().ForbidPrune(bc.genesisBlock.Root()) + if head := bc.CurrentBlock(); head.NumberU64() > 0 { + bc.gcsave = head.Root() + bc.stateCache.TrieDB().ForbidPrune(bc.gcsave) + } // Take ownership of this particular state go bc.update() return bc, nil @@ -718,8 +726,8 @@ func (bc *BlockChain) Stop() { } } } - for !bc.triegc.Empty() { - triedb.Dereference(bc.triegc.PopItem().(common.Hash), false) + for !bc.gcqueue.Empty() { + triedb.Dereference(bc.gcqueue.PopItem().(common.Hash), false) } if size, _ := triedb.Size(); size != 0 { log.Error("Dangling trie nodes after full cleanup", "size", size) @@ -967,7 +975,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. } else { // Full but not archive node, do proper garbage collection triedb.Reference(common.Hash{}, root, common.Hash{}) // metadata reference to keep trie alive - bc.triegc.Push(root, -int64(block.NumberU64())) + bc.gcqueue.Push(root, -int64(block.NumberU64())) if current := block.NumberU64(); current > triesInMemory { // If we exceeded our memory allowance, flush matured singleton nodes to disk @@ -998,13 +1006,21 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. triedb.Commit(header.Root, true) lastWrite = chosen bc.gcproc = 0 + + // A new snapshot was flushed to disk, swap the prune allowance + triedb.ForbidPrune(header.Root) + if bc.gcsave != (common.Hash{}) { + triedb.PermitPrune(bc.gcsave) + triedb.Dereference(bc.gcsave, true) + } + bc.gcsave = header.Root } } // Garbage collect anything below our required write retention - for !bc.triegc.Empty() { - root, number := bc.triegc.Pop() + for !bc.gcqueue.Empty() { + root, number := bc.gcqueue.Pop() if uint64(-number) > chosen { - bc.triegc.Push(root, number) + bc.gcqueue.Push(root, number) break } triedb.Dereference(root.(common.Hash), true) diff --git a/trie/database.go b/trie/database.go index effd7708f6..50ddfbf8ff 100644 --- a/trie/database.go +++ b/trie/database.go @@ -101,7 +101,8 @@ func splitNodeKey(key string) (common.Hash, common.Hash) { // the disk database. The aim is to accumulate trie writes in-memory and only // periodically flush a couple tries to disk, garbage collecting the remainder. type Database struct { - diskdb ethdb.Database // Persistent storage for matured trie nodes + diskdb ethdb.Database // Persistent storage for matured trie nodes + noprune map[common.Hash]struct{} // Root hashes of the tries that aren't prunable cleans *bigcache.BigCache // GC friendly memory cache of clean node RLPs dirties map[string]*cachedNode // Data and references relationships of dirty nodes @@ -363,12 +364,34 @@ func NewDatabaseWithCache(diskdb ethdb.Database, cache int) *Database { } return &Database{ diskdb: diskdb, + noprune: make(map[common.Hash]struct{}), cleans: cleans, dirties: map[string]*cachedNode{metaRoot: {}}, preimages: make(map[common.Hash][]byte), } } +// ForbidPrune adds a root hash to the list of tries that are disallowed from +// being pruned. The only two ever to be used are the genesis trie and the last +// committed trie (snapshot). +func (db *Database) ForbidPrune(root common.Hash) { + db.lock.Lock() + defer db.lock.Unlock() + + log.Debug("Forbidding pruner to delete trie", "root", root) + db.noprune[root] = struct{}{} +} + +// PermitPrune allows a particular trie to be pruned from the disk database. To +// actually run the pruner, please call a dereference on the root hash. +func (db *Database) PermitPrune(root common.Hash) { + db.lock.Lock() + defer db.lock.Unlock() + + log.Debug("Permitting pruner to delete trie", "root", root) + delete(db.noprune, root) +} + // DiskDB retrieves the persistent storage backing the trie database. func (db *Database) DiskDB() DatabaseReader { return db.diskdb @@ -459,7 +482,7 @@ func (db *Database) node(owner common.Hash, hash common.Hash, cachegen uint16) n return nil } if db.cleans != nil { - db.cleans.Set(string(hash[:]), enc) + db.cleans.Set(key, enc) memcacheCleanMissMeter.Mark(1) memcacheCleanWriteMeter.Mark(int64(len(enc))) } diff --git a/trie/database_pruning.go b/trie/database_pruning.go index eb3d42c722..3278b62936 100644 --- a/trie/database_pruning.go +++ b/trie/database_pruning.go @@ -77,6 +77,16 @@ func (p *pruner) execute() { }, }) } + // Beside all the tries kept in memory, keep anything forbidden from pruning + for hash := range p.db.noprune { + p.tries = append(p.tries, &traverser{ + db: p.db, + state: &traverserState{ + node: hashNode(common.CopyBytes(hash[:])), // Need closure, take care!! + hash: hash, + }, + }) + } // Iterate over all the nodes marked for pruning and delete them for _, mark := range p.marks { p.prune(mark.owner, mark.hash, mark.path) @@ -169,17 +179,13 @@ func (t *traverser) live(owner common.Hash, hash common.Hash, path []byte, unref // Short circuit the liveness check if we already covered this prefix (if this // prefix path was not yet seen in previous tries, no parent could have been // seen either, so no point in checkin upwards further than the first hash). - state := t.state - for state != nil { - // If we've found a hash node, check if it's an already known result + for state := t.state; state != nil; state = state.parent { if state.hash != (common.Hash{}) { if unrefs[state.hash] { return false } break } - // Not a hash node, traverse further up - state = state.parent } // Traverse downward until the prefix matches the path completely path = path[len(t.state.prefix):] @@ -206,7 +212,7 @@ func (t *traverser) live(owner common.Hash, hash common.Hash, path []byte, unref } else { blob, err := t.db.diskdb.Get([]byte(key)) if blob == nil || err != nil { - log.Error("Missing referenced node", "owner", owner, "hash", t.state.hash, "path", fmt.Sprintf("%x%x", t.state.prefix, path)) + log.Error("Missing referenced node", "owner", owner, "hash", t.state.hash.Hex(), "path", fmt.Sprintf("%x%x", t.state.prefix, path)) return false //panic(fmt.Sprintf("missing referenced node %x (searching for %x:%x at %x%x)", key, owner, t.state.hash, t.state.prefix, path)) }