github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/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(goerli 274 // and some other private networks), it's possible that two 275 // consecutive blocks will have same root. In this case snapshot 276 // difflayer won't be created. So HEAD-127 may not paired with 277 // head-127 layer. Instead the paired layer is higher than the 278 // bottom-most diff layer. Try to find the bottom-most snapshot 279 // layer with state available. 280 // 281 // Note HEAD and HEAD-1 is ignored. Usually there is the associated 282 // state available, but we don't want to use the topmost state 283 // as the pruning target. 284 var found bool 285 for i := len(layers) - 2; i >= 2; i-- { 286 if rawdb.HasLegacyTrieNode(p.db, layers[i].Root()) { 287 root = layers[i].Root() 288 found = true 289 log.Info("Selecting middle-layer as the pruning target", "root", root, "depth", i) 290 break 291 } 292 } 293 if !found { 294 if len(layers) > 0 { 295 return errors.New("no snapshot paired state") 296 } 297 return fmt.Errorf("associated state[%x] is not present", root) 298 } 299 } else { 300 if len(layers) > 0 { 301 log.Info("Selecting bottom-most difflayer as the pruning target", "root", root, "height", p.chainHeader.Number.Uint64()-127) 302 } else { 303 log.Info("Selecting user-specified state as the pruning target", "root", root) 304 } 305 } 306 // All the state roots of the middle layer should be forcibly pruned, 307 // otherwise the dangling state will be left. 308 middleRoots := make(map[common.Hash]struct{}) 309 for _, layer := range layers { 310 if layer.Root() == root { 311 break 312 } 313 middleRoots[layer.Root()] = struct{}{} 314 } 315 // Traverse the target state, re-construct the whole state trie and 316 // commit to the given bloom filter. 317 start := time.Now() 318 if err := snapshot.GenerateTrie(p.snaptree, root, p.db, p.stateBloom); err != nil { 319 return err 320 } 321 // Traverse the genesis, put all genesis state entries into the 322 // bloom filter too. 323 if err := extractGenesis(p.db, p.stateBloom); err != nil { 324 return err 325 } 326 filterName := bloomFilterName(p.config.Datadir, root) 327 328 log.Info("Writing state bloom to disk", "name", filterName) 329 if err := p.stateBloom.Commit(filterName, filterName+stateBloomFileTempSuffix); err != nil { 330 return err 331 } 332 log.Info("State bloom filter committed", "name", filterName) 333 return prune(p.snaptree, root, p.db, p.stateBloom, filterName, middleRoots, start) 334 } 335 336 // RecoverPruning will resume the pruning procedure during the system restart. 337 // This function is used in this case: user tries to prune state data, but the 338 // system was interrupted midway because of crash or manual-kill. In this case 339 // if the bloom filter for filtering active state is already constructed, the 340 // pruning can be resumed. What's more if the bloom filter is constructed, the 341 // pruning **has to be resumed**. Otherwise a lot of dangling nodes may be left 342 // in the disk. 343 func RecoverPruning(datadir string, db ethdb.Database) error { 344 stateBloomPath, stateBloomRoot, err := findBloomFilter(datadir) 345 if err != nil { 346 return err 347 } 348 if stateBloomPath == "" { 349 return nil // nothing to recover 350 } 351 headBlock := rawdb.ReadHeadBlock(db) 352 if headBlock == nil { 353 return errors.New("failed to load head block") 354 } 355 // Initialize the snapshot tree in recovery mode to handle this special case: 356 // - Users run the `prune-state` command multiple times 357 // - Neither these `prune-state` running is finished(e.g. interrupted manually) 358 // - The state bloom filter is already generated, a part of state is deleted, 359 // so that resuming the pruning here is mandatory 360 // - The state HEAD is rewound already because of multiple incomplete `prune-state` 361 // In this case, even the state HEAD is not exactly matched with snapshot, it 362 // still feasible to recover the pruning correctly. 363 snapconfig := snapshot.Config{ 364 CacheSize: 256, 365 Recovery: true, 366 NoBuild: true, 367 AsyncBuild: false, 368 } 369 // Offline pruning is only supported in legacy hash based scheme. 370 triedb := triedb.NewDatabase(db, triedb.HashDefaults) 371 snaptree, err := snapshot.New(snapconfig, db, triedb, headBlock.Root()) 372 if err != nil { 373 return err // The relevant snapshot(s) might not exist 374 } 375 stateBloom, err := NewStateBloomFromDisk(stateBloomPath) 376 if err != nil { 377 return err 378 } 379 log.Info("Loaded state bloom filter", "path", stateBloomPath) 380 381 // All the state roots of the middle layers should be forcibly pruned, 382 // otherwise the dangling state will be left. 383 var ( 384 found bool 385 layers = snaptree.Snapshots(headBlock.Root(), 128, true) 386 middleRoots = make(map[common.Hash]struct{}) 387 ) 388 for _, layer := range layers { 389 if layer.Root() == stateBloomRoot { 390 found = true 391 break 392 } 393 middleRoots[layer.Root()] = struct{}{} 394 } 395 if !found { 396 log.Error("Pruning target state is not existent") 397 return errors.New("non-existent target state") 398 } 399 return prune(snaptree, stateBloomRoot, db, stateBloom, stateBloomPath, middleRoots, time.Now()) 400 } 401 402 // extractGenesis loads the genesis state and commits all the state entries 403 // into the given bloomfilter. 404 func extractGenesis(db ethdb.Database, stateBloom *stateBloom) error { 405 genesisHash := rawdb.ReadCanonicalHash(db, 0) 406 if genesisHash == (common.Hash{}) { 407 return errors.New("missing genesis hash") 408 } 409 genesis := rawdb.ReadBlock(db, genesisHash, 0) 410 if genesis == nil { 411 return errors.New("missing genesis block") 412 } 413 t, err := trie.NewStateTrie(trie.StateTrieID(genesis.Root()), triedb.NewDatabase(db, triedb.HashDefaults)) 414 if err != nil { 415 return err 416 } 417 accIter, err := t.NodeIterator(nil) 418 if err != nil { 419 return err 420 } 421 for accIter.Next(true) { 422 hash := accIter.Hash() 423 424 // Embedded nodes don't have hash. 425 if hash != (common.Hash{}) { 426 stateBloom.Put(hash.Bytes(), nil) 427 } 428 // If it's a leaf node, yes we are touching an account, 429 // dig into the storage trie further. 430 if accIter.Leaf() { 431 var acc types.StateAccount 432 if err := rlp.DecodeBytes(accIter.LeafBlob(), &acc); err != nil { 433 return err 434 } 435 if acc.Root != types.EmptyRootHash { 436 id := trie.StorageTrieID(genesis.Root(), common.BytesToHash(accIter.LeafKey()), acc.Root) 437 storageTrie, err := trie.NewStateTrie(id, triedb.NewDatabase(db, triedb.HashDefaults)) 438 if err != nil { 439 return err 440 } 441 storageIter, err := storageTrie.NodeIterator(nil) 442 if err != nil { 443 return err 444 } 445 for storageIter.Next(true) { 446 hash := storageIter.Hash() 447 if hash != (common.Hash{}) { 448 stateBloom.Put(hash.Bytes(), nil) 449 } 450 } 451 if storageIter.Error() != nil { 452 return storageIter.Error() 453 } 454 } 455 if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) { 456 stateBloom.Put(acc.CodeHash, nil) 457 } 458 } 459 } 460 return accIter.Error() 461 } 462 463 func bloomFilterName(datadir string, hash common.Hash) string { 464 return filepath.Join(datadir, fmt.Sprintf("%s.%s.%s", stateBloomFilePrefix, hash.Hex(), stateBloomFileSuffix)) 465 } 466 467 func isBloomFilter(filename string) (bool, common.Hash) { 468 filename = filepath.Base(filename) 469 if strings.HasPrefix(filename, stateBloomFilePrefix) && strings.HasSuffix(filename, stateBloomFileSuffix) { 470 return true, common.HexToHash(filename[len(stateBloomFilePrefix)+1 : len(filename)-len(stateBloomFileSuffix)-1]) 471 } 472 return false, common.Hash{} 473 } 474 475 func findBloomFilter(datadir string) (string, common.Hash, error) { 476 var ( 477 stateBloomPath string 478 stateBloomRoot common.Hash 479 ) 480 if err := filepath.Walk(datadir, func(path string, info os.FileInfo, err error) error { 481 if info != nil && !info.IsDir() { 482 ok, root := isBloomFilter(path) 483 if ok { 484 stateBloomPath = path 485 stateBloomRoot = root 486 } 487 } 488 return nil 489 }); err != nil { 490 return "", common.Hash{}, err 491 } 492 return stateBloomPath, stateBloomRoot, nil 493 }