github.com/MetalBlockchain/subnet-evm@v0.4.9/core/state/pruner/pruner.go (about) 1 // (c) 2019-2020, Ava Labs, Inc. 2 // 3 // This file is a derived work, based on the go-ethereum library whose original 4 // notices appear below. 5 // 6 // It is distributed under a license compatible with the licensing terms of the 7 // original code from which it is derived. 8 // 9 // Much love to the original authors for their work. 10 // ********** 11 // Copyright 2021 The go-ethereum Authors 12 // This file is part of the go-ethereum library. 13 // 14 // The go-ethereum library is free software: you can redistribute it and/or modify 15 // it under the terms of the GNU Lesser General Public License as published by 16 // the Free Software Foundation, either version 3 of the License, or 17 // (at your option) any later version. 18 // 19 // The go-ethereum library is distributed in the hope that it will be useful, 20 // but WITHOUT ANY WARRANTY; without even the implied warranty of 21 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 // GNU Lesser General Public License for more details. 23 // 24 // You should have received a copy of the GNU Lesser General Public License 25 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 26 27 package pruner 28 29 import ( 30 "bytes" 31 "encoding/binary" 32 "errors" 33 "fmt" 34 "math" 35 "os" 36 "path/filepath" 37 "strings" 38 "time" 39 40 "github.com/MetalBlockchain/subnet-evm/core/rawdb" 41 "github.com/MetalBlockchain/subnet-evm/core/state/snapshot" 42 "github.com/MetalBlockchain/subnet-evm/core/types" 43 "github.com/MetalBlockchain/subnet-evm/ethdb" 44 "github.com/MetalBlockchain/subnet-evm/trie" 45 "github.com/ethereum/go-ethereum/common" 46 "github.com/ethereum/go-ethereum/crypto" 47 "github.com/ethereum/go-ethereum/log" 48 "github.com/ethereum/go-ethereum/rlp" 49 ) 50 51 const ( 52 // stateBloomFilePrefix is the filename prefix of state bloom filter. 53 stateBloomFilePrefix = "statebloom" 54 55 // stateBloomFilePrefix is the filename suffix of state bloom filter. 56 stateBloomFileSuffix = "bf.gz" 57 58 // stateBloomFileTempSuffix is the filename suffix of state bloom filter 59 // while it is being written out to detect write aborts. 60 stateBloomFileTempSuffix = ".tmp" 61 62 // rangeCompactionThreshold is the minimal deleted entry number for 63 // triggering range compaction. It's a quite arbitrary number but just 64 // to avoid triggering range compaction because of small deletion. 65 rangeCompactionThreshold = 100000 66 ) 67 68 var ( 69 // emptyRoot is the known root hash of an empty trie. 70 emptyRoot = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") 71 72 // emptyCode is the known hash of the empty EVM bytecode. 73 emptyCode = crypto.Keccak256(nil) 74 ) 75 76 // Pruner is an offline tool to prune the stale state with the 77 // help of the snapshot. The workflow of pruner is very simple: 78 // 79 // - iterate the snapshot, reconstruct the relevant state 80 // - iterate the database, delete all other state entries which 81 // don't belong to the target state and the genesis state 82 // 83 // It can take several hours(around 2 hours for mainnet) to finish 84 // the whole pruning work. It's recommended to run this offline tool 85 // periodically in order to release the disk usage and improve the 86 // disk read performance to some extent. 87 type Pruner struct { 88 db ethdb.Database 89 stateBloom *stateBloom 90 datadir string 91 headHeader *types.Header 92 snaptree *snapshot.Tree 93 } 94 95 // NewPruner creates the pruner instance. 96 func NewPruner(db ethdb.Database, datadir string, bloomSize uint64) (*Pruner, error) { 97 headBlock := rawdb.ReadHeadBlock(db) 98 if headBlock == nil { 99 return nil, errors.New("Failed to load head block") 100 } 101 // Note: we refuse to start a pruning session unless the snapshot disk layer exists, which should prevent 102 // us from ever needing to enter RecoverPruning in an invalid pruning session (a session where we do not have 103 // the protected trie in the triedb and in the snapshot disk layer). 104 snaptree, err := snapshot.New(db, trie.NewDatabase(db), 256, headBlock.Hash(), headBlock.Root(), false, false, false) 105 if err != nil { 106 return nil, fmt.Errorf("failed to create snapshot for pruning, must restart without offline pruning disabled to recover: %w", err) // The relevant snapshot(s) might not exist 107 } 108 // Sanitize the bloom filter size if it's too small. 109 if bloomSize < 256 { 110 log.Warn("Sanitizing bloomfilter size", "provided(MB)", bloomSize, "updated(MB)", 256) 111 bloomSize = 256 112 } 113 stateBloom, err := newStateBloomWithSize(bloomSize) 114 if err != nil { 115 return nil, err 116 } 117 return &Pruner{ 118 db: db, 119 stateBloom: stateBloom, 120 datadir: datadir, 121 headHeader: headBlock.Header(), 122 snaptree: snaptree, 123 }, nil 124 } 125 126 func prune(maindb ethdb.Database, stateBloom *stateBloom, bloomPath string, 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 // We wrap iter.Release() in an anonymous function so that the [iter] 143 // value captured is the value of [iter] at the end of the function as opposed 144 // to incorrectly capturing the first iterator immediately. 145 defer func() { 146 iter.Release() 147 }() 148 149 for iter.Next() { 150 key := iter.Key() 151 152 // All state entries don't belong to specific state and genesis are deleted here 153 // - trie node 154 // - legacy contract code 155 // - new-scheme contract code 156 isCode, codeKey := rawdb.IsCodeKey(key) 157 if len(key) == common.HashLength || isCode { 158 checkKey := key 159 if isCode { 160 checkKey = codeKey 161 } 162 if ok, err := stateBloom.Contain(checkKey); err != nil { 163 return err 164 } else if ok { 165 continue 166 } 167 count += 1 168 size += common.StorageSize(len(key) + len(iter.Value())) 169 if err := batch.Delete(key); err != nil { 170 return err 171 } 172 173 var eta time.Duration // Realistically will never remain uninited 174 if done := binary.BigEndian.Uint64(key[:8]); done > 0 { 175 var ( 176 left = math.MaxUint64 - binary.BigEndian.Uint64(key[:8]) 177 speed = done/uint64(time.Since(pstart)/time.Millisecond+1) + 1 // +1s to avoid division by zero 178 ) 179 eta = time.Duration(left/speed) * time.Millisecond 180 } 181 if time.Since(logged) > 8*time.Second { 182 log.Info("Pruning state data", "nodes", count, "size", size, 183 "elapsed", common.PrettyDuration(time.Since(pstart)), "eta", common.PrettyDuration(eta)) 184 logged = time.Now() 185 } 186 // Recreate the iterator after every batch commit in order 187 // to allow the underlying compactor to delete the entries. 188 if batch.ValueSize() >= ethdb.IdealBatchSize { 189 if err := batch.Write(); err != nil { 190 return err 191 } 192 batch.Reset() 193 194 iter.Release() 195 iter = maindb.NewIterator(nil, key) 196 } 197 } 198 } 199 if err := iter.Error(); err != nil { 200 return fmt.Errorf("failed to iterate db during pruning: %w", err) 201 } 202 if batch.ValueSize() > 0 { 203 if err := batch.Write(); err != nil { 204 return err 205 } 206 batch.Reset() 207 } 208 iter.Release() 209 log.Info("Pruned state data", "nodes", count, "size", size, "elapsed", common.PrettyDuration(time.Since(pstart))) 210 211 // Write marker to DB to indicate offline pruning finished successfully. We write before calling os.RemoveAll 212 // to guarantee that if the node dies midway through pruning, then this will run during RecoverPruning. 213 if err := rawdb.WriteOfflinePruning(maindb); err != nil { 214 return fmt.Errorf("failed to write offline pruning success marker: %w", err) 215 } 216 217 // Delete the state bloom, it marks the entire pruning procedure is 218 // finished. If any crashes or manual exit happens before this, 219 // `RecoverPruning` will pick it up in the next restarts to redo all 220 // the things. 221 if err := os.RemoveAll(bloomPath); err != nil { 222 return fmt.Errorf("failed to remove bloom filter from disk: %w", err) 223 } 224 225 // Start compactions, will remove the deleted data from the disk immediately. 226 // Note for small pruning, the compaction is skipped. 227 if count >= rangeCompactionThreshold { 228 cstart := time.Now() 229 for b := 0x00; b <= 0xf0; b += 0x10 { 230 var ( 231 start = []byte{byte(b)} 232 end = []byte{byte(b + 0x10)} 233 ) 234 if b == 0xf0 { 235 end = nil 236 } 237 log.Info("Compacting database", "range", fmt.Sprintf("%#x-%#x", start, end), "elapsed", common.PrettyDuration(time.Since(cstart))) 238 if err := maindb.Compact(start, end); err != nil { 239 log.Error("Database compaction failed", "error", err) 240 return err 241 } 242 } 243 log.Info("Database compaction finished", "elapsed", common.PrettyDuration(time.Since(cstart))) 244 } 245 log.Info("State pruning successful", "pruned", size, "elapsed", common.PrettyDuration(time.Since(start))) 246 return nil 247 } 248 249 // Prune deletes all historical state nodes except the nodes belong to the 250 // specified state version. If user doesn't specify the state version, use 251 // the bottom-most snapshot diff layer as the target. 252 func (p *Pruner) Prune(root common.Hash) error { 253 // If the state bloom filter is already committed previously, 254 // reuse it for pruning instead of generating a new one. It's 255 // mandatory because a part of state may already be deleted, 256 // the recovery procedure is necessary. 257 _, stateBloomRoot, err := findBloomFilter(p.datadir) 258 if err != nil { 259 return err 260 } 261 if stateBloomRoot != (common.Hash{}) { 262 return RecoverPruning(p.datadir, p.db) 263 } 264 265 // If the target state root is not specified, return a fatal error. 266 if root == (common.Hash{}) { 267 return fmt.Errorf("cannot prune with an empty root: %s", 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.HasTrieNode(p.db, root) { 273 return fmt.Errorf("associated state[%x] is not present", root) 274 } else { 275 log.Info("Selecting last accepted block root as the pruning target", "root", root) 276 } 277 278 // Traverse the target state, re-construct the whole state trie and 279 // commit to the given bloom filter. 280 start := time.Now() 281 if err := snapshot.GenerateTrie(p.snaptree, root, p.db, p.stateBloom); err != nil { 282 return err 283 } 284 // Traverse the genesis, put all genesis state entries into the 285 // bloom filter too. 286 if err := extractGenesis(p.db, p.stateBloom); err != nil { 287 return err 288 } 289 filterName := bloomFilterName(p.datadir, root) 290 291 log.Info("Writing state bloom to disk", "name", filterName) 292 if err := p.stateBloom.Commit(filterName, filterName+stateBloomFileTempSuffix); err != nil { 293 return err 294 } 295 log.Info("State bloom filter committed", "name", filterName) 296 return prune(p.db, p.stateBloom, filterName, start) 297 } 298 299 // RecoverPruning will resume the pruning procedure during the system restart. 300 // This function is used in this case: user tries to prune state data, but the 301 // system was interrupted midway because of crash or manual-kill. In this case 302 // if the bloom filter for filtering active state is already constructed, the 303 // pruning can be resumed. What's more if the bloom filter is constructed, the 304 // pruning **has to be resumed**. Otherwise a lot of dangling nodes may be left 305 // in the disk. 306 func RecoverPruning(datadir string, db ethdb.Database) error { 307 stateBloomPath, stateBloomRoot, err := findBloomFilter(datadir) 308 if err != nil { 309 return err 310 } 311 if stateBloomPath == "" { 312 return nil // nothing to recover 313 } 314 headBlock := rawdb.ReadHeadBlock(db) 315 if headBlock == nil { 316 return errors.New("Failed to load head block") 317 } 318 stateBloom, err := NewStateBloomFromDisk(stateBloomPath) 319 if err != nil { 320 return err 321 } 322 log.Info("Loaded state bloom filter", "path", stateBloomPath) 323 324 // All the state roots of the middle layers should be forcibly pruned, 325 // otherwise the dangling state will be left. 326 if stateBloomRoot != headBlock.Root() { 327 return fmt.Errorf("cannot recover pruning to state bloom root: %s, with head block root: %s", stateBloomRoot, headBlock.Root()) 328 } 329 330 return prune(db, stateBloom, stateBloomPath, time.Now()) 331 } 332 333 // extractGenesis loads the genesis state and commits all the state entries 334 // into the given bloomfilter. 335 func extractGenesis(db ethdb.Database, stateBloom *stateBloom) error { 336 genesisHash := rawdb.ReadCanonicalHash(db, 0) 337 if genesisHash == (common.Hash{}) { 338 return errors.New("missing genesis hash") 339 } 340 genesis := rawdb.ReadBlock(db, genesisHash, 0) 341 if genesis == nil { 342 return errors.New("missing genesis block") 343 } 344 t, err := trie.NewStateTrie(common.Hash{}, genesis.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 types.StateAccount 360 if err := rlp.DecodeBytes(accIter.LeafBlob(), &acc); err != nil { 361 return err 362 } 363 if acc.Root != emptyRoot { 364 storageTrie, err := trie.NewStateTrie(common.BytesToHash(accIter.LeafKey()), 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, 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 }