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