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