core, trie: prevent genesis and last snapshot from being pruned

This commit is contained in:
Péter Szilágyi 2019-01-10 10:51:49 +02:00
parent 25a4d2e067
commit 17e7433725
No known key found for this signature in database
GPG key ID: E9AE538CEDF8293D
3 changed files with 63 additions and 18 deletions

View file

@ -95,9 +95,11 @@ type BlockChain struct {
chainConfig *params.ChainConfig // Chain & network configuration chainConfig *params.ChainConfig // Chain & network configuration
cacheConfig *CacheConfig // Cache configuration for pruning cacheConfig *CacheConfig // Cache configuration for pruning
db ethdb.Database // Low level persistent database to store final content in 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 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 hc *HeaderChain
rmLogsFeed event.Feed rmLogsFeed event.Feed
@ -159,7 +161,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par
chainConfig: chainConfig, chainConfig: chainConfig,
cacheConfig: cacheConfig, cacheConfig: cacheConfig,
db: db, db: db,
triegc: prque.New(nil), gcqueue: prque.New(nil),
stateCache: state.NewDatabaseWithCache(db, cacheConfig.TrieCleanLimit), stateCache: state.NewDatabaseWithCache(db, cacheConfig.TrieCleanLimit),
quit: make(chan struct{}), quit: make(chan struct{}),
shouldPreserve: shouldPreserve, 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 // Take ownership of this particular state
go bc.update() go bc.update()
return bc, nil return bc, nil
@ -718,8 +726,8 @@ func (bc *BlockChain) Stop() {
} }
} }
} }
for !bc.triegc.Empty() { for !bc.gcqueue.Empty() {
triedb.Dereference(bc.triegc.PopItem().(common.Hash), false) triedb.Dereference(bc.gcqueue.PopItem().(common.Hash), false)
} }
if size, _ := triedb.Size(); size != 0 { if size, _ := triedb.Size(); size != 0 {
log.Error("Dangling trie nodes after full cleanup", "size", size) log.Error("Dangling trie nodes after full cleanup", "size", size)
@ -967,7 +975,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
} else { } else {
// Full but not archive node, do proper garbage collection // Full but not archive node, do proper garbage collection
triedb.Reference(common.Hash{}, root, common.Hash{}) // metadata reference to keep trie alive 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 current := block.NumberU64(); current > triesInMemory {
// If we exceeded our memory allowance, flush matured singleton nodes to disk // 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) triedb.Commit(header.Root, true)
lastWrite = chosen lastWrite = chosen
bc.gcproc = 0 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 // Garbage collect anything below our required write retention
for !bc.triegc.Empty() { for !bc.gcqueue.Empty() {
root, number := bc.triegc.Pop() root, number := bc.gcqueue.Pop()
if uint64(-number) > chosen { if uint64(-number) > chosen {
bc.triegc.Push(root, number) bc.gcqueue.Push(root, number)
break break
} }
triedb.Dereference(root.(common.Hash), true) triedb.Dereference(root.(common.Hash), true)

View file

@ -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 // 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. // periodically flush a couple tries to disk, garbage collecting the remainder.
type Database struct { 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 cleans *bigcache.BigCache // GC friendly memory cache of clean node RLPs
dirties map[string]*cachedNode // Data and references relationships of dirty nodes 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{ return &Database{
diskdb: diskdb, diskdb: diskdb,
noprune: make(map[common.Hash]struct{}),
cleans: cleans, cleans: cleans,
dirties: map[string]*cachedNode{metaRoot: {}}, dirties: map[string]*cachedNode{metaRoot: {}},
preimages: make(map[common.Hash][]byte), 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. // DiskDB retrieves the persistent storage backing the trie database.
func (db *Database) DiskDB() DatabaseReader { func (db *Database) DiskDB() DatabaseReader {
return db.diskdb return db.diskdb
@ -459,7 +482,7 @@ func (db *Database) node(owner common.Hash, hash common.Hash, cachegen uint16) n
return nil return nil
} }
if db.cleans != nil { if db.cleans != nil {
db.cleans.Set(string(hash[:]), enc) db.cleans.Set(key, enc)
memcacheCleanMissMeter.Mark(1) memcacheCleanMissMeter.Mark(1)
memcacheCleanWriteMeter.Mark(int64(len(enc))) memcacheCleanWriteMeter.Mark(int64(len(enc)))
} }

View file

@ -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 // Iterate over all the nodes marked for pruning and delete them
for _, mark := range p.marks { for _, mark := range p.marks {
p.prune(mark.owner, mark.hash, mark.path) 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 // 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 // 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). // seen either, so no point in checkin upwards further than the first hash).
state := t.state for state := t.state; state != nil; state = state.parent {
for state != nil {
// If we've found a hash node, check if it's an already known result
if state.hash != (common.Hash{}) { if state.hash != (common.Hash{}) {
if unrefs[state.hash] { if unrefs[state.hash] {
return false return false
} }
break break
} }
// Not a hash node, traverse further up
state = state.parent
} }
// Traverse downward until the prefix matches the path completely // Traverse downward until the prefix matches the path completely
path = path[len(t.state.prefix):] path = path[len(t.state.prefix):]
@ -206,7 +212,7 @@ func (t *traverser) live(owner common.Hash, hash common.Hash, path []byte, unref
} else { } else {
blob, err := t.db.diskdb.Get([]byte(key)) blob, err := t.db.diskdb.Get([]byte(key))
if blob == nil || err != nil { 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 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)) //panic(fmt.Sprintf("missing referenced node %x (searching for %x:%x at %x%x)", key, owner, t.state.hash, t.state.prefix, path))
} }