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