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