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