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