github.com/theQRL/go-zond@v0.1.1/core/state/pruner/pruner.go (about) 1 // Copyright 2021 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package pruner 18 19 import ( 20 "bytes" 21 "encoding/binary" 22 "errors" 23 "fmt" 24 "math" 25 "os" 26 "path/filepath" 27 "strings" 28 "time" 29 30 "github.com/theQRL/go-zond/common" 31 "github.com/theQRL/go-zond/core/rawdb" 32 "github.com/theQRL/go-zond/core/state/snapshot" 33 "github.com/theQRL/go-zond/core/types" 34 "github.com/theQRL/go-zond/log" 35 "github.com/theQRL/go-zond/rlp" 36 "github.com/theQRL/go-zond/trie" 37 "github.com/theQRL/go-zond/zonddb" 38 ) 39 40 const ( 41 // stateBloomFilePrefix is the filename prefix of state bloom filter. 42 stateBloomFilePrefix = "statebloom" 43 44 // stateBloomFilePrefix is the filename suffix of state bloom filter. 45 stateBloomFileSuffix = "bf.gz" 46 47 // stateBloomFileTempSuffix is the filename suffix of state bloom filter 48 // while it is being written out to detect write aborts. 49 stateBloomFileTempSuffix = ".tmp" 50 51 // rangeCompactionThreshold is the minimal deleted entry number for 52 // triggering range compaction. It's a quite arbitrary number but just 53 // to avoid triggering range compaction because of small deletion. 54 rangeCompactionThreshold = 100000 55 ) 56 57 // Config includes all the configurations for pruning. 58 type Config struct { 59 Datadir string // The directory of the state database 60 BloomSize uint64 // The Megabytes of memory allocated to bloom-filter 61 } 62 63 // Pruner is an offline tool to prune the stale state with the 64 // help of the snapshot. The workflow of pruner is very simple: 65 // 66 // - iterate the snapshot, reconstruct the relevant state 67 // - iterate the database, delete all other state entries which 68 // don't belong to the target state and the genesis state 69 // 70 // It can take several hours(around 2 hours for mainnet) to finish 71 // the whole pruning work. It's recommended to run this offline tool 72 // periodically in order to release the disk usage and improve the 73 // disk read performance to some extent. 74 type Pruner struct { 75 config Config 76 chainHeader *types.Header 77 db zonddb.Database 78 stateBloom *stateBloom 79 snaptree *snapshot.Tree 80 } 81 82 // NewPruner creates the pruner instance. 83 func NewPruner(db zonddb.Database, config Config) (*Pruner, error) { 84 headBlock := rawdb.ReadHeadBlock(db) 85 if headBlock == nil { 86 return nil, errors.New("failed to load head block") 87 } 88 // Offline pruning is only supported in legacy hash based scheme. 89 triedb := trie.NewDatabase(db, trie.HashDefaults) 90 91 snapconfig := snapshot.Config{ 92 CacheSize: 256, 93 Recovery: false, 94 NoBuild: true, 95 AsyncBuild: false, 96 } 97 snaptree, err := snapshot.New(snapconfig, db, triedb, headBlock.Root()) 98 if err != nil { 99 return nil, err // The relevant snapshot(s) might not exist 100 } 101 // Sanitize the bloom filter size if it's too small. 102 if config.BloomSize < 256 { 103 log.Warn("Sanitizing bloomfilter size", "provided(MB)", config.BloomSize, "updated(MB)", 256) 104 config.BloomSize = 256 105 } 106 stateBloom, err := newStateBloomWithSize(config.BloomSize) 107 if err != nil { 108 return nil, err 109 } 110 return &Pruner{ 111 config: config, 112 chainHeader: headBlock.Header(), 113 db: db, 114 stateBloom: stateBloom, 115 snaptree: snaptree, 116 }, nil 117 } 118 119 func prune(snaptree *snapshot.Tree, root common.Hash, maindb zonddb.Database, stateBloom *stateBloom, bloomPath string, middleStateRoots map[common.Hash]struct{}, start time.Time) error { 120 // Delete all stale trie nodes in the disk. With the help of state bloom 121 // the trie nodes(and codes) belong to the active state will be filtered 122 // out. A very small part of stale tries will also be filtered because of 123 // the false-positive rate of bloom filter. But the assumption is held here 124 // that the false-positive is low enough(~0.05%). The probablity of the 125 // dangling node is the state root is super low. So the dangling nodes in 126 // theory will never ever be visited again. 127 var ( 128 count int 129 size common.StorageSize 130 pstart = time.Now() 131 logged = time.Now() 132 batch = maindb.NewBatch() 133 iter = maindb.NewIterator(nil, nil) 134 ) 135 for iter.Next() { 136 key := iter.Key() 137 138 // All state entries don't belong to specific state and genesis are deleted here 139 // - trie node 140 // - legacy contract code 141 // - new-scheme contract code 142 isCode, codeKey := rawdb.IsCodeKey(key) 143 if len(key) == common.HashLength || isCode { 144 checkKey := key 145 if isCode { 146 checkKey = codeKey 147 } 148 if _, exist := middleStateRoots[common.BytesToHash(checkKey)]; exist { 149 log.Debug("Forcibly delete the middle state roots", "hash", common.BytesToHash(checkKey)) 150 } else { 151 if stateBloom.Contain(checkKey) { 152 continue 153 } 154 } 155 count += 1 156 size += common.StorageSize(len(key) + len(iter.Value())) 157 batch.Delete(key) 158 159 var eta time.Duration // Realistically will never remain uninited 160 if done := binary.BigEndian.Uint64(key[:8]); done > 0 { 161 var ( 162 left = math.MaxUint64 - binary.BigEndian.Uint64(key[:8]) 163 speed = done/uint64(time.Since(pstart)/time.Millisecond+1) + 1 // +1s to avoid division by zero 164 ) 165 eta = time.Duration(left/speed) * time.Millisecond 166 } 167 if time.Since(logged) > 8*time.Second { 168 log.Info("Pruning state data", "nodes", count, "size", size, 169 "elapsed", common.PrettyDuration(time.Since(pstart)), "eta", common.PrettyDuration(eta)) 170 logged = time.Now() 171 } 172 // Recreate the iterator after every batch commit in order 173 // to allow the underlying compactor to delete the entries. 174 if batch.ValueSize() >= zonddb.IdealBatchSize { 175 batch.Write() 176 batch.Reset() 177 178 iter.Release() 179 iter = maindb.NewIterator(nil, key) 180 } 181 } 182 } 183 if batch.ValueSize() > 0 { 184 batch.Write() 185 batch.Reset() 186 } 187 iter.Release() 188 log.Info("Pruned state data", "nodes", count, "size", size, "elapsed", common.PrettyDuration(time.Since(pstart))) 189 190 // Pruning is done, now drop the "useless" layers from the snapshot. 191 // Firstly, flushing the target layer into the disk. After that all 192 // diff layers below the target will all be merged into the disk. 193 if err := snaptree.Cap(root, 0); err != nil { 194 return err 195 } 196 // Secondly, flushing the snapshot journal into the disk. All diff 197 // layers upon are dropped silently. Eventually the entire snapshot 198 // tree is converted into a single disk layer with the pruning target 199 // as the root. 200 if _, err := snaptree.Journal(root); err != nil { 201 return err 202 } 203 // Delete the state bloom, it marks the entire pruning procedure is 204 // finished. If any crashes or manual exit happens before this, 205 // `RecoverPruning` will pick it up in the next restarts to redo all 206 // the things. 207 os.RemoveAll(bloomPath) 208 209 // Start compactions, will remove the deleted data from the disk immediately. 210 // Note for small pruning, the compaction is skipped. 211 if count >= rangeCompactionThreshold { 212 cstart := time.Now() 213 for b := 0x00; b <= 0xf0; b += 0x10 { 214 var ( 215 start = []byte{byte(b)} 216 end = []byte{byte(b + 0x10)} 217 ) 218 if b == 0xf0 { 219 end = nil 220 } 221 log.Info("Compacting database", "range", fmt.Sprintf("%#x-%#x", start, end), "elapsed", common.PrettyDuration(time.Since(cstart))) 222 if err := maindb.Compact(start, end); err != nil { 223 log.Error("Database compaction failed", "error", err) 224 return err 225 } 226 } 227 log.Info("Database compaction finished", "elapsed", common.PrettyDuration(time.Since(cstart))) 228 } 229 log.Info("State pruning successful", "pruned", size, "elapsed", common.PrettyDuration(time.Since(start))) 230 return nil 231 } 232 233 // Prune deletes all historical state nodes except the nodes belong to the 234 // specified state version. If user doesn't specify the state version, use 235 // the bottom-most snapshot diff layer as the target. 236 func (p *Pruner) Prune(root common.Hash) error { 237 // If the state bloom filter is already committed previously, 238 // reuse it for pruning instead of generating a new one. It's 239 // mandatory because a part of state may already be deleted, 240 // the recovery procedure is necessary. 241 _, stateBloomRoot, err := findBloomFilter(p.config.Datadir) 242 if err != nil { 243 return err 244 } 245 if stateBloomRoot != (common.Hash{}) { 246 return RecoverPruning(p.config.Datadir, p.db) 247 } 248 // If the target state root is not specified, use the HEAD-127 as the 249 // target. The reason for picking it is: 250 // - in most of the normal cases, the related state is available 251 // - the probability of this layer being reorg is very low 252 var layers []snapshot.Snapshot 253 if root == (common.Hash{}) { 254 // Retrieve all snapshot layers from the current HEAD. 255 // In theory there are 128 difflayers + 1 disk layer present, 256 // so 128 diff layers are expected to be returned. 257 layers = p.snaptree.Snapshots(p.chainHeader.Root, 128, true) 258 if len(layers) != 128 { 259 // Reject if the accumulated diff layers are less than 128. It 260 // means in most of normal cases, there is no associated state 261 // with bottom-most diff layer. 262 return fmt.Errorf("snapshot not old enough yet: need %d more blocks", 128-len(layers)) 263 } 264 // Use the bottom-most diff layer as the target 265 root = layers[len(layers)-1].Root() 266 } 267 // Ensure the root is really present. The weak assumption 268 // is the presence of root can indicate the presence of the 269 // entire trie. 270 if !rawdb.HasLegacyTrieNode(p.db, root) { 271 // The special case is for clique based networks(goerli 272 // and some other private networks), it's possible that two 273 // consecutive blocks will have same root. In this case snapshot 274 // difflayer won't be created. So HEAD-127 may not paired with 275 // head-127 layer. Instead the paired layer is higher than the 276 // bottom-most diff layer. Try to find the bottom-most snapshot 277 // layer with state available. 278 // 279 // Note HEAD and HEAD-1 is ignored. Usually there is the associated 280 // state available, but we don't want to use the topmost state 281 // as the pruning target. 282 var found bool 283 for i := len(layers) - 2; i >= 2; i-- { 284 if rawdb.HasLegacyTrieNode(p.db, layers[i].Root()) { 285 root = layers[i].Root() 286 found = true 287 log.Info("Selecting middle-layer as the pruning target", "root", root, "depth", i) 288 break 289 } 290 } 291 if !found { 292 if len(layers) > 0 { 293 return errors.New("no snapshot paired state") 294 } 295 return fmt.Errorf("associated state[%x] is not present", root) 296 } 297 } else { 298 if len(layers) > 0 { 299 log.Info("Selecting bottom-most difflayer as the pruning target", "root", root, "height", p.chainHeader.Number.Uint64()-127) 300 } else { 301 log.Info("Selecting user-specified state as the pruning target", "root", root) 302 } 303 } 304 // All the state roots of the middle layer should be forcibly pruned, 305 // otherwise the dangling state will be left. 306 middleRoots := make(map[common.Hash]struct{}) 307 for _, layer := range layers { 308 if layer.Root() == root { 309 break 310 } 311 middleRoots[layer.Root()] = struct{}{} 312 } 313 // Traverse the target state, re-construct the whole state trie and 314 // commit to the given bloom filter. 315 start := time.Now() 316 if err := snapshot.GenerateTrie(p.snaptree, root, p.db, p.stateBloom); err != nil { 317 return err 318 } 319 // Traverse the genesis, put all genesis state entries into the 320 // bloom filter too. 321 if err := extractGenesis(p.db, p.stateBloom); err != nil { 322 return err 323 } 324 filterName := bloomFilterName(p.config.Datadir, root) 325 326 log.Info("Writing state bloom to disk", "name", filterName) 327 if err := p.stateBloom.Commit(filterName, filterName+stateBloomFileTempSuffix); err != nil { 328 return err 329 } 330 log.Info("State bloom filter committed", "name", filterName) 331 return prune(p.snaptree, root, p.db, p.stateBloom, filterName, middleRoots, start) 332 } 333 334 // RecoverPruning will resume the pruning procedure during the system restart. 335 // This function is used in this case: user tries to prune state data, but the 336 // system was interrupted midway because of crash or manual-kill. In this case 337 // if the bloom filter for filtering active state is already constructed, the 338 // pruning can be resumed. What's more if the bloom filter is constructed, the 339 // pruning **has to be resumed**. Otherwise a lot of dangling nodes may be left 340 // in the disk. 341 func RecoverPruning(datadir string, db zonddb.Database) error { 342 stateBloomPath, stateBloomRoot, err := findBloomFilter(datadir) 343 if err != nil { 344 return err 345 } 346 if stateBloomPath == "" { 347 return nil // nothing to recover 348 } 349 headBlock := rawdb.ReadHeadBlock(db) 350 if headBlock == nil { 351 return errors.New("failed to load head block") 352 } 353 // Initialize the snapshot tree in recovery mode to handle this special case: 354 // - Users run the `prune-state` command multiple times 355 // - Neither these `prune-state` running is finished(e.g. interrupted manually) 356 // - The state bloom filter is already generated, a part of state is deleted, 357 // so that resuming the pruning here is mandatory 358 // - The state HEAD is rewound already because of multiple incomplete `prune-state` 359 // In this case, even the state HEAD is not exactly matched with snapshot, it 360 // still feasible to recover the pruning correctly. 361 snapconfig := snapshot.Config{ 362 CacheSize: 256, 363 Recovery: true, 364 NoBuild: true, 365 AsyncBuild: false, 366 } 367 // Offline pruning is only supported in legacy hash based scheme. 368 triedb := trie.NewDatabase(db, trie.HashDefaults) 369 snaptree, err := snapshot.New(snapconfig, db, triedb, headBlock.Root()) 370 if err != nil { 371 return err // The relevant snapshot(s) might not exist 372 } 373 stateBloom, err := NewStateBloomFromDisk(stateBloomPath) 374 if err != nil { 375 return err 376 } 377 log.Info("Loaded state bloom filter", "path", stateBloomPath) 378 379 // All the state roots of the middle layers should be forcibly pruned, 380 // otherwise the dangling state will be left. 381 var ( 382 found bool 383 layers = snaptree.Snapshots(headBlock.Root(), 128, true) 384 middleRoots = make(map[common.Hash]struct{}) 385 ) 386 for _, layer := range layers { 387 if layer.Root() == stateBloomRoot { 388 found = true 389 break 390 } 391 middleRoots[layer.Root()] = struct{}{} 392 } 393 if !found { 394 log.Error("Pruning target state is not existent") 395 return errors.New("non-existent target state") 396 } 397 return prune(snaptree, stateBloomRoot, db, stateBloom, stateBloomPath, middleRoots, time.Now()) 398 } 399 400 // extractGenesis loads the genesis state and commits all the state entries 401 // into the given bloomfilter. 402 func extractGenesis(db zonddb.Database, stateBloom *stateBloom) error { 403 genesisHash := rawdb.ReadCanonicalHash(db, 0) 404 if genesisHash == (common.Hash{}) { 405 return errors.New("missing genesis hash") 406 } 407 genesis := rawdb.ReadBlock(db, genesisHash, 0) 408 if genesis == nil { 409 return errors.New("missing genesis block") 410 } 411 t, err := trie.NewStateTrie(trie.StateTrieID(genesis.Root()), trie.NewDatabase(db, trie.HashDefaults)) 412 if err != nil { 413 return err 414 } 415 accIter, err := t.NodeIterator(nil) 416 if err != nil { 417 return err 418 } 419 for accIter.Next(true) { 420 hash := accIter.Hash() 421 422 // Embedded nodes don't have hash. 423 if hash != (common.Hash{}) { 424 stateBloom.Put(hash.Bytes(), nil) 425 } 426 // If it's a leaf node, yes we are touching an account, 427 // dig into the storage trie further. 428 if accIter.Leaf() { 429 var acc types.StateAccount 430 if err := rlp.DecodeBytes(accIter.LeafBlob(), &acc); err != nil { 431 return err 432 } 433 if acc.Root != types.EmptyRootHash { 434 id := trie.StorageTrieID(genesis.Root(), common.BytesToHash(accIter.LeafKey()), acc.Root) 435 storageTrie, err := trie.NewStateTrie(id, trie.NewDatabase(db, trie.HashDefaults)) 436 if err != nil { 437 return err 438 } 439 storageIter, err := storageTrie.NodeIterator(nil) 440 if err != nil { 441 return err 442 } 443 for storageIter.Next(true) { 444 hash := storageIter.Hash() 445 if hash != (common.Hash{}) { 446 stateBloom.Put(hash.Bytes(), nil) 447 } 448 } 449 if storageIter.Error() != nil { 450 return storageIter.Error() 451 } 452 } 453 if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) { 454 stateBloom.Put(acc.CodeHash, nil) 455 } 456 } 457 } 458 return accIter.Error() 459 } 460 461 func bloomFilterName(datadir string, hash common.Hash) string { 462 return filepath.Join(datadir, fmt.Sprintf("%s.%s.%s", stateBloomFilePrefix, hash.Hex(), stateBloomFileSuffix)) 463 } 464 465 func isBloomFilter(filename string) (bool, common.Hash) { 466 filename = filepath.Base(filename) 467 if strings.HasPrefix(filename, stateBloomFilePrefix) && strings.HasSuffix(filename, stateBloomFileSuffix) { 468 return true, common.HexToHash(filename[len(stateBloomFilePrefix)+1 : len(filename)-len(stateBloomFileSuffix)-1]) 469 } 470 return false, common.Hash{} 471 } 472 473 func findBloomFilter(datadir string) (string, common.Hash, error) { 474 var ( 475 stateBloomPath string 476 stateBloomRoot common.Hash 477 ) 478 if err := filepath.Walk(datadir, func(path string, info os.FileInfo, err error) error { 479 if info != nil && !info.IsDir() { 480 ok, root := isBloomFilter(path) 481 if ok { 482 stateBloomPath = path 483 stateBloomRoot = root 484 } 485 } 486 return nil 487 }); err != nil { 488 return "", common.Hash{}, err 489 } 490 return stateBloomPath, stateBloomRoot, nil 491 }