github.com/unicornultrafoundation/go-u2u@v1.0.0-rc1.0.20240205080301-e74a83d3fadc/gossip/evmstore/evmpruner/pruner.go (about)

     1  // Copyright 2020 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 evmpruner
    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/unicornultrafoundation/go-u2u/common"
    31  	"github.com/unicornultrafoundation/go-u2u/core/rawdb"
    32  	"github.com/unicornultrafoundation/go-u2u/core/state"
    33  	"github.com/unicornultrafoundation/go-u2u/core/state/snapshot"
    34  	"github.com/unicornultrafoundation/go-u2u/core/types"
    35  	"github.com/unicornultrafoundation/go-u2u/ethdb"
    36  	"github.com/unicornultrafoundation/go-u2u/log"
    37  	"github.com/unicornultrafoundation/go-u2u/rlp"
    38  	"github.com/unicornultrafoundation/go-u2u/trie"
    39  
    40  	"github.com/unicornultrafoundation/go-u2u/gossip/evmstore"
    41  )
    42  
    43  const (
    44  	// stateBloomFilePrefix is the filename prefix of state bloom filter.
    45  	stateBloomFilePrefix = "statebloom"
    46  
    47  	// stateBloomFilePrefix is the filename suffix of state bloom filter.
    48  	stateBloomFileSuffix = "bf.gz"
    49  
    50  	// stateBloomFileTempSuffix is the filename suffix of state bloom filter
    51  	// while it is being written out to detect write aborts.
    52  	stateBloomFileTempSuffix = ".tmp"
    53  
    54  	// rangeCompactionThreshold is the minimal deleted entry number for
    55  	// triggering range compaction. It's a quite arbitrary number but just
    56  	// to avoid triggering range compaction because of small deletion.
    57  	rangeCompactionThreshold = 100000
    58  )
    59  
    60  // Pruner is an offline tool to prune the stale state with the
    61  // help of the snapshot. The workflow of pruner is very simple:
    62  //
    63  //   - iterate the snapshot, reconstruct the relevant state
    64  //   - iterate the database, delete all other state entries which
    65  //     don't belong to the target state and the genesis state
    66  //
    67  // It can take several hours(around 2 hours for mainnet) to finish
    68  // the whole pruning work. It's recommended to run this offline tool
    69  // periodically in order to release the disk usage and improve the
    70  // disk read performance to some extent.
    71  type Pruner struct {
    72  	db          ethdb.Database
    73  	stateBloom  StateBloom
    74  	datadir     string
    75  	root        common.Hash
    76  	genesisRoot common.Hash
    77  	snaptree    *snapshot.Tree
    78  }
    79  
    80  type StateBloom interface {
    81  	ethdb.KeyValueWriter
    82  	Contain(key []byte) (bool, error)
    83  	Commit(filename, tempname string) error
    84  }
    85  
    86  func NewProbabilisticSet(bloomSize uint64) (StateBloom, error) {
    87  	// Sanitize the bloom filter size if it's too small.
    88  	if bloomSize < 256 {
    89  		log.Warn("Sanitizing bloomfilter size", "provided(MB)", bloomSize, "updated(MB)", 256)
    90  		bloomSize = 256
    91  	}
    92  	return newStateBloomWithSize(bloomSize)
    93  }
    94  
    95  // NewPruner creates the pruner instance.
    96  func NewPruner(db ethdb.Database, genesisRoot, root common.Hash, datadir string, stateBloom StateBloom) (*Pruner, error) {
    97  	snaptree, err := snapshot.New(db, trie.NewDatabase(db), 256, root, false, false, false)
    98  	if err != nil {
    99  		return nil, err // The relevant snapshot(s) might not exist
   100  	}
   101  	return &Pruner{
   102  		db:          db,
   103  		stateBloom:  stateBloom,
   104  		datadir:     datadir,
   105  		root:        root,
   106  		genesisRoot: genesisRoot,
   107  		snaptree:    snaptree,
   108  	}, nil
   109  }
   110  
   111  func prune(snaptree *snapshot.Tree, root common.Hash, maindb ethdb.Database, stateBloom StateBloom, bloomPath string, middleStateRoots map[common.Hash]struct{}, start time.Time) error {
   112  	// Delete all stale trie nodes in the disk. With the help of state bloom
   113  	// the trie nodes(and codes) belong to the active state will be filtered
   114  	// out. A very small part of stale tries will also be filtered because of
   115  	// the false-positive rate of bloom filter. But the assumption is held here
   116  	// that the false-positive is low enough(~0.05%). The probablity of the
   117  	// dangling node is the state root is super low. So the dangling nodes in
   118  	// theory will never ever be visited again.
   119  	var (
   120  		count  int
   121  		size   common.StorageSize
   122  		pstart = time.Now()
   123  		logged = time.Now()
   124  		batch  = maindb.NewBatch()
   125  		iter   = maindb.NewIterator(nil, nil)
   126  	)
   127  	for iter.Next() {
   128  		key := iter.Key()
   129  
   130  		// All state entries don't belong to specific state and genesis are deleted here
   131  		// - trie node
   132  		// - legacy contract code
   133  		// - new-scheme contract code
   134  		isCode, codeKey := rawdb.IsCodeKey(key)
   135  		if len(key) == common.HashLength || isCode {
   136  			checkKey := key
   137  			if isCode {
   138  				checkKey = codeKey
   139  			}
   140  			if _, exist := middleStateRoots[common.BytesToHash(checkKey)]; exist {
   141  				log.Debug("Forcibly delete the middle state roots", "hash", common.BytesToHash(checkKey))
   142  			} else {
   143  				if ok, err := stateBloom.Contain(checkKey); err != nil {
   144  					return err
   145  				} else if ok {
   146  					continue
   147  				}
   148  			}
   149  			count += 1
   150  			size += common.StorageSize(len(key) + len(iter.Value()))
   151  			batch.Delete(key)
   152  
   153  			var eta time.Duration // Realistically will never remain uninited
   154  			if done := binary.BigEndian.Uint64(key[:8]); done > 0 {
   155  				var (
   156  					left  = math.MaxUint64 - binary.BigEndian.Uint64(key[:8])
   157  					speed = done/uint64(time.Since(pstart)/time.Millisecond+1) + 1 // +1s to avoid division by zero
   158  				)
   159  				eta = time.Duration(left/speed) * time.Millisecond
   160  			}
   161  			if time.Since(logged) > 8*time.Second {
   162  				log.Info("Pruning state data", "nodes", count, "size", size,
   163  					"elapsed", common.PrettyDuration(time.Since(pstart)), "eta", common.PrettyDuration(eta))
   164  				logged = time.Now()
   165  			}
   166  			// Recreate the iterator after every batch commit in order
   167  			// to allow the underlying compactor to delete the entries.
   168  			if batch.ValueSize() >= ethdb.IdealBatchSize {
   169  				batch.Write()
   170  				batch.Reset()
   171  
   172  				iter.Release()
   173  				iter = maindb.NewIterator(nil, key)
   174  			}
   175  		}
   176  	}
   177  	if batch.ValueSize() > 0 {
   178  		batch.Write()
   179  		batch.Reset()
   180  	}
   181  	iter.Release()
   182  	log.Info("Pruned state data", "nodes", count, "size", size, "elapsed", common.PrettyDuration(time.Since(pstart)))
   183  
   184  	// Secondly, flushing the snapshot journal into the disk. All diff
   185  	// layers upon are dropped silently. Eventually the entire snapshot
   186  	// tree is converted into a single disk layer with the pruning target
   187  	// as the root.
   188  	if _, err := snaptree.Journal(root); err != nil {
   189  		return err
   190  	}
   191  	// Delete the state bloom, it marks the entire pruning procedure is
   192  	// finished. If any crashes or manual exit happens before this,
   193  	// `RecoverPruning` will pick it up in the next restarts to redo all
   194  	// the things.
   195  	os.RemoveAll(bloomPath)
   196  
   197  	// Start compactions, will remove the deleted data from the disk immediately.
   198  	// Note for small pruning, the compaction is skipped.
   199  	if count >= rangeCompactionThreshold {
   200  		cstart := time.Now()
   201  		for b := 0x00; b <= 0xf0; b += 0x10 {
   202  			var (
   203  				start = []byte{byte(b)}
   204  				end   = []byte{byte(b + 0x10)}
   205  			)
   206  			if b == 0xf0 {
   207  				end = nil
   208  			}
   209  			log.Info("Compacting database", "range", fmt.Sprintf("%#x-%#x", start, end), "elapsed", common.PrettyDuration(time.Since(cstart)))
   210  			if err := maindb.Compact(start, end); err != nil {
   211  				log.Error("Database compaction failed", "error", err)
   212  				return err
   213  			}
   214  		}
   215  		log.Info("Database compaction finished", "elapsed", common.PrettyDuration(time.Since(cstart)))
   216  	}
   217  	log.Info("State pruning successful", "pruned", size, "elapsed", common.PrettyDuration(time.Since(start)))
   218  	return nil
   219  }
   220  
   221  // Prune deletes all historical state nodes except the nodes belong to the
   222  // specified state version. If user doesn't specify the state version, use
   223  // the bottom-most snapshot diff layer as the target.
   224  func (p *Pruner) Prune(root common.Hash) error {
   225  	// If the state bloom filter is already committed previously,
   226  	// reuse it for pruning instead of generating a new one. It's
   227  	// mandatory because a part of state may already be deleted,
   228  	// the recovery procedure is necessary.
   229  	_, stateBloomRoot, err := findBloomFilter(p.datadir)
   230  	if err != nil {
   231  		return err
   232  	}
   233  	if stateBloomRoot != (common.Hash{}) {
   234  		return RecoverPruning(p.datadir, p.db, p.root)
   235  	}
   236  	// If the target state root is not specified, use the HEAD-127 as the
   237  	// target. The reason for picking it is:
   238  	// - in most of the normal cases, the related state is available
   239  	// - the probability of this layer being reorg is very low
   240  	var layers []snapshot.Snapshot
   241  	if root == (common.Hash{}) {
   242  		// Retrieve all snapshot layers from the current HEAD.
   243  		// In theory there are 128 difflayers + 1 disk layer present,
   244  		// so 128 diff layers are expected to be returned.
   245  		layers = p.snaptree.Snapshots(p.root, 1, false)
   246  		if len(layers) <= 0 {
   247  			// Reject if the accumulated diff layers are less than 128. It
   248  			// means in most of normal cases, there is no associated state
   249  			// with bottom-most diff layer.
   250  			return fmt.Errorf("snapshot not old enough yet: need %d more blocks", 1)
   251  		}
   252  		// Use the bottom-most diff layer as the target
   253  		root = layers[len(layers)-1].Root()
   254  	}
   255  
   256  	// All the state roots of the middle layer should be forcibly pruned,
   257  	// otherwise the dangling state will be left.
   258  	middleRoots := make(map[common.Hash]struct{})
   259  	for _, layer := range layers {
   260  		if layer.Root() == root {
   261  			break
   262  		}
   263  		middleRoots[layer.Root()] = struct{}{}
   264  	}
   265  	// Traverse the target state, re-construct the whole state trie and
   266  	// commit to the given bloom filter.
   267  	start := time.Now()
   268  	if err := snapshot.GenerateTrie(p.snaptree, root, p.db, p.stateBloom); err != nil {
   269  		return err
   270  	}
   271  	// Traverse the genesis, put all genesis state entries into the
   272  	// bloom filter too.
   273  	if err := extractGenesis(p.db, p.genesisRoot, p.stateBloom); err != nil {
   274  		return err
   275  	}
   276  	filterName := bloomFilterName(p.datadir, root)
   277  
   278  	if err := p.stateBloom.Commit(filterName, filterName+stateBloomFileTempSuffix); err != nil {
   279  		return err
   280  	}
   281  	return prune(p.snaptree, root, p.db, p.stateBloom, filterName, middleRoots, start)
   282  }
   283  
   284  // RecoverPruning will resume the pruning procedure during the system restart.
   285  // This function is used in this case: user tries to prune state data, but the
   286  // system was interrupted midway because of crash or manual-kill. In this case
   287  // if the bloom filter for filtering active state is already constructed, the
   288  // pruning can be resumed. What's more if the bloom filter is constructed, the
   289  // pruning **has to be resumed**. Otherwise a lot of dangling nodes may be left
   290  // in the disk.
   291  func RecoverPruning(datadir string, db ethdb.Database, root common.Hash) error {
   292  	stateBloomPath, stateBloomRoot, err := findBloomFilter(datadir)
   293  	if err != nil {
   294  		return err
   295  	}
   296  	if stateBloomPath == "" {
   297  		return nil // nothing to recover
   298  	}
   299  	// Initialize the snapshot tree in recovery mode to handle this special case:
   300  	// - Users run the `prune-state` command multiple times
   301  	// - Neither these `prune-state` running is finished(e.g. interrupted manually)
   302  	// - The state bloom filter is already generated, a part of state is deleted,
   303  	//   so that resuming the pruning here is mandatory
   304  	// - The state HEAD is rewound already because of multiple incomplete `prune-state`
   305  	// In this case, even the state HEAD is not exactly matched with snapshot, it
   306  	// still feasible to recover the pruning correctly.
   307  	snaptree, err := snapshot.New(db, trie.NewDatabase(db), 256, root, false, false, true)
   308  	if err != nil {
   309  		return err // The relevant snapshot(s) might not exist
   310  	}
   311  	stateBloom, err := NewStateBloomFromDisk(stateBloomPath)
   312  	if err != nil {
   313  		return err
   314  	}
   315  	log.Info("Loaded state bloom filter", "path", stateBloomPath)
   316  
   317  	// All the state roots of the middle layers should be forcibly pruned,
   318  	// otherwise the dangling state will be left.
   319  	var (
   320  		found       bool
   321  		layers      = snaptree.Snapshots(root, 1, true)
   322  		middleRoots = make(map[common.Hash]struct{})
   323  	)
   324  	for _, layer := range layers {
   325  		if layer.Root() == stateBloomRoot {
   326  			found = true
   327  			break
   328  		}
   329  		middleRoots[layer.Root()] = struct{}{}
   330  	}
   331  	if !found {
   332  		log.Error("Pruning target state is not existent")
   333  		return errors.New("non-existent target state")
   334  	}
   335  	return prune(snaptree, stateBloomRoot, db, stateBloom, stateBloomPath, middleRoots, time.Now())
   336  }
   337  
   338  // extractGenesis loads the genesis state and commits all the state entries
   339  // into the given bloomfilter.
   340  func extractGenesis(db ethdb.Database, root common.Hash, stateBloom ethdb.KeyValueWriter) error {
   341  	if root == (common.Hash{}) {
   342  		return nil
   343  	}
   344  	t, err := trie.NewSecure(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 state.Account
   360  			if err := rlp.DecodeBytes(accIter.LeafBlob(), &acc); err != nil {
   361  				return err
   362  			}
   363  			if acc.Root != types.EmptyRootHash {
   364  				storageTrie, err := trie.NewSecure(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, evmstore.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  }
   418  
   419  const warningLog = `
   420  
   421  WARNING!
   422  
   423  The clean trie cache is not found. Please delete it by yourself after the 
   424  pruning. Remember don't start the Geth without deleting the clean trie cache
   425  otherwise the entire database may be damaged!
   426  
   427  Check the command description "geth snapshot prune-state --help" for more details.
   428  `
   429  
   430  func deleteCleanTrieCache(path string) {
   431  	if _, err := os.Stat(path); os.IsNotExist(err) {
   432  		log.Warn(warningLog)
   433  		return
   434  	}
   435  	os.RemoveAll(path)
   436  	log.Info("Deleted trie clean cache", "path", path)
   437  }