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