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  }