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