github.com/ethw3/go-ethereuma@v0.0.0-20221013053120-c14602a4c23c/core/rawdb/database.go (about)

     1  // Copyright 2018 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 rawdb
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"os"
    24  	"path"
    25  	"sync/atomic"
    26  	"time"
    27  
    28  	"github.com/ethw3/go-ethereuma/common"
    29  	"github.com/ethw3/go-ethereuma/ethdb"
    30  	"github.com/ethw3/go-ethereuma/ethdb/leveldb"
    31  	"github.com/ethw3/go-ethereuma/ethdb/memorydb"
    32  	"github.com/ethw3/go-ethereuma/log"
    33  	"github.com/olekukonko/tablewriter"
    34  )
    35  
    36  // freezerdb is a database wrapper that enabled freezer data retrievals.
    37  type freezerdb struct {
    38  	ancientRoot string
    39  	ethdb.KeyValueStore
    40  	ethdb.AncientStore
    41  }
    42  
    43  // AncientDatadir returns the path of root ancient directory.
    44  func (frdb *freezerdb) AncientDatadir() (string, error) {
    45  	return frdb.ancientRoot, nil
    46  }
    47  
    48  // Close implements io.Closer, closing both the fast key-value store as well as
    49  // the slow ancient tables.
    50  func (frdb *freezerdb) Close() error {
    51  	var errs []error
    52  	if err := frdb.AncientStore.Close(); err != nil {
    53  		errs = append(errs, err)
    54  	}
    55  	if err := frdb.KeyValueStore.Close(); err != nil {
    56  		errs = append(errs, err)
    57  	}
    58  	if len(errs) != 0 {
    59  		return fmt.Errorf("%v", errs)
    60  	}
    61  	return nil
    62  }
    63  
    64  // Freeze is a helper method used for external testing to trigger and block until
    65  // a freeze cycle completes, without having to sleep for a minute to trigger the
    66  // automatic background run.
    67  func (frdb *freezerdb) Freeze(threshold uint64) error {
    68  	if frdb.AncientStore.(*chainFreezer).readonly {
    69  		return errReadOnly
    70  	}
    71  	// Set the freezer threshold to a temporary value
    72  	defer func(old uint64) {
    73  		atomic.StoreUint64(&frdb.AncientStore.(*chainFreezer).threshold, old)
    74  	}(atomic.LoadUint64(&frdb.AncientStore.(*chainFreezer).threshold))
    75  	atomic.StoreUint64(&frdb.AncientStore.(*chainFreezer).threshold, threshold)
    76  
    77  	// Trigger a freeze cycle and block until it's done
    78  	trigger := make(chan struct{}, 1)
    79  	frdb.AncientStore.(*chainFreezer).trigger <- trigger
    80  	<-trigger
    81  	return nil
    82  }
    83  
    84  // nofreezedb is a database wrapper that disables freezer data retrievals.
    85  type nofreezedb struct {
    86  	ethdb.KeyValueStore
    87  }
    88  
    89  // HasAncient returns an error as we don't have a backing chain freezer.
    90  func (db *nofreezedb) HasAncient(kind string, number uint64) (bool, error) {
    91  	return false, errNotSupported
    92  }
    93  
    94  // Ancient returns an error as we don't have a backing chain freezer.
    95  func (db *nofreezedb) Ancient(kind string, number uint64) ([]byte, error) {
    96  	return nil, errNotSupported
    97  }
    98  
    99  // AncientRange returns an error as we don't have a backing chain freezer.
   100  func (db *nofreezedb) AncientRange(kind string, start, max, maxByteSize uint64) ([][]byte, error) {
   101  	return nil, errNotSupported
   102  }
   103  
   104  // Ancients returns an error as we don't have a backing chain freezer.
   105  func (db *nofreezedb) Ancients() (uint64, error) {
   106  	return 0, errNotSupported
   107  }
   108  
   109  // Tail returns an error as we don't have a backing chain freezer.
   110  func (db *nofreezedb) Tail() (uint64, error) {
   111  	return 0, errNotSupported
   112  }
   113  
   114  // AncientSize returns an error as we don't have a backing chain freezer.
   115  func (db *nofreezedb) AncientSize(kind string) (uint64, error) {
   116  	return 0, errNotSupported
   117  }
   118  
   119  // ModifyAncients is not supported.
   120  func (db *nofreezedb) ModifyAncients(func(ethdb.AncientWriteOp) error) (int64, error) {
   121  	return 0, errNotSupported
   122  }
   123  
   124  // TruncateHead returns an error as we don't have a backing chain freezer.
   125  func (db *nofreezedb) TruncateHead(items uint64) error {
   126  	return errNotSupported
   127  }
   128  
   129  // TruncateTail returns an error as we don't have a backing chain freezer.
   130  func (db *nofreezedb) TruncateTail(items uint64) error {
   131  	return errNotSupported
   132  }
   133  
   134  // Sync returns an error as we don't have a backing chain freezer.
   135  func (db *nofreezedb) Sync() error {
   136  	return errNotSupported
   137  }
   138  
   139  func (db *nofreezedb) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err error) {
   140  	// Unlike other ancient-related methods, this method does not return
   141  	// errNotSupported when invoked.
   142  	// The reason for this is that the caller might want to do several things:
   143  	// 1. Check if something is in freezer,
   144  	// 2. If not, check leveldb.
   145  	//
   146  	// This will work, since the ancient-checks inside 'fn' will return errors,
   147  	// and the leveldb work will continue.
   148  	//
   149  	// If we instead were to return errNotSupported here, then the caller would
   150  	// have to explicitly check for that, having an extra clause to do the
   151  	// non-ancient operations.
   152  	return fn(db)
   153  }
   154  
   155  // MigrateTable processes the entries in a given table in sequence
   156  // converting them to a new format if they're of an old format.
   157  func (db *nofreezedb) MigrateTable(kind string, convert convertLegacyFn) error {
   158  	return errNotSupported
   159  }
   160  
   161  // AncientDatadir returns an error as we don't have a backing chain freezer.
   162  func (db *nofreezedb) AncientDatadir() (string, error) {
   163  	return "", errNotSupported
   164  }
   165  
   166  // NewDatabase creates a high level database on top of a given key-value data
   167  // store without a freezer moving immutable chain segments into cold storage.
   168  func NewDatabase(db ethdb.KeyValueStore) ethdb.Database {
   169  	return &nofreezedb{KeyValueStore: db}
   170  }
   171  
   172  // resolveChainFreezerDir is a helper function which resolves the absolute path
   173  // of chain freezer by considering backward compatibility.
   174  func resolveChainFreezerDir(ancient string) string {
   175  	// Check if the chain freezer is already present in the specified
   176  	// sub folder, if not then two possibilities:
   177  	// - chain freezer is not initialized
   178  	// - chain freezer exists in legacy location (root ancient folder)
   179  	freezer := path.Join(ancient, chainFreezerName)
   180  	if !common.FileExist(freezer) {
   181  		if !common.FileExist(ancient) {
   182  			// The entire ancient store is not initialized, still use the sub
   183  			// folder for initialization.
   184  		} else {
   185  			// Ancient root is already initialized, then we hold the assumption
   186  			// that chain freezer is also initialized and located in root folder.
   187  			// In this case fallback to legacy location.
   188  			freezer = ancient
   189  			log.Info("Found legacy ancient chain path", "location", ancient)
   190  		}
   191  	}
   192  	return freezer
   193  }
   194  
   195  // NewDatabaseWithFreezer creates a high level database on top of a given key-
   196  // value data store with a freezer moving immutable chain segments into cold
   197  // storage. The passed ancient indicates the path of root ancient directory
   198  // where the chain freezer can be opened.
   199  func NewDatabaseWithFreezer(db ethdb.KeyValueStore, ancient string, namespace string, readonly bool) (ethdb.Database, error) {
   200  	// Create the idle freezer instance
   201  	frdb, err := newChainFreezer(resolveChainFreezerDir(ancient), namespace, readonly, freezerTableSize, chainFreezerNoSnappy)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  	// Since the freezer can be stored separately from the user's key-value database,
   206  	// there's a fairly high probability that the user requests invalid combinations
   207  	// of the freezer and database. Ensure that we don't shoot ourselves in the foot
   208  	// by serving up conflicting data, leading to both datastores getting corrupted.
   209  	//
   210  	//   - If both the freezer and key-value store is empty (no genesis), we just
   211  	//     initialized a new empty freezer, so everything's fine.
   212  	//   - If the key-value store is empty, but the freezer is not, we need to make
   213  	//     sure the user's genesis matches the freezer. That will be checked in the
   214  	//     blockchain, since we don't have the genesis block here (nor should we at
   215  	//     this point care, the key-value/freezer combo is valid).
   216  	//   - If neither the key-value store nor the freezer is empty, cross validate
   217  	//     the genesis hashes to make sure they are compatible. If they are, also
   218  	//     ensure that there's no gap between the freezer and subsequently leveldb.
   219  	//   - If the key-value store is not empty, but the freezer is we might just be
   220  	//     upgrading to the freezer release, or we might have had a small chain and
   221  	//     not frozen anything yet. Ensure that no blocks are missing yet from the
   222  	//     key-value store, since that would mean we already had an old freezer.
   223  
   224  	// If the genesis hash is empty, we have a new key-value store, so nothing to
   225  	// validate in this method. If, however, the genesis hash is not nil, compare
   226  	// it to the freezer content.
   227  	if kvgenesis, _ := db.Get(headerHashKey(0)); len(kvgenesis) > 0 {
   228  		if frozen, _ := frdb.Ancients(); frozen > 0 {
   229  			// If the freezer already contains something, ensure that the genesis blocks
   230  			// match, otherwise we might mix up freezers across chains and destroy both
   231  			// the freezer and the key-value store.
   232  			frgenesis, err := frdb.Ancient(chainFreezerHashTable, 0)
   233  			if err != nil {
   234  				return nil, fmt.Errorf("failed to retrieve genesis from ancient %v", err)
   235  			} else if !bytes.Equal(kvgenesis, frgenesis) {
   236  				return nil, fmt.Errorf("genesis mismatch: %#x (leveldb) != %#x (ancients)", kvgenesis, frgenesis)
   237  			}
   238  			// Key-value store and freezer belong to the same network. Ensure that they
   239  			// are contiguous, otherwise we might end up with a non-functional freezer.
   240  			if kvhash, _ := db.Get(headerHashKey(frozen)); len(kvhash) == 0 {
   241  				// Subsequent header after the freezer limit is missing from the database.
   242  				// Reject startup if the database has a more recent head.
   243  				if *ReadHeaderNumber(db, ReadHeadHeaderHash(db)) > frozen-1 {
   244  					return nil, fmt.Errorf("gap (#%d) in the chain between ancients and leveldb", frozen)
   245  				}
   246  				// Database contains only older data than the freezer, this happens if the
   247  				// state was wiped and reinited from an existing freezer.
   248  			}
   249  			// Otherwise, key-value store continues where the freezer left off, all is fine.
   250  			// We might have duplicate blocks (crash after freezer write but before key-value
   251  			// store deletion, but that's fine).
   252  		} else {
   253  			// If the freezer is empty, ensure nothing was moved yet from the key-value
   254  			// store, otherwise we'll end up missing data. We check block #1 to decide
   255  			// if we froze anything previously or not, but do take care of databases with
   256  			// only the genesis block.
   257  			if ReadHeadHeaderHash(db) != common.BytesToHash(kvgenesis) {
   258  				// Key-value store contains more data than the genesis block, make sure we
   259  				// didn't freeze anything yet.
   260  				if kvblob, _ := db.Get(headerHashKey(1)); len(kvblob) == 0 {
   261  					return nil, errors.New("ancient chain segments already extracted, please set --datadir.ancient to the correct path")
   262  				}
   263  				// Block #1 is still in the database, we're allowed to init a new freezer
   264  			}
   265  			// Otherwise, the head header is still the genesis, we're allowed to init a new
   266  			// freezer.
   267  		}
   268  	}
   269  	// Freezer is consistent with the key-value database, permit combining the two
   270  	if !frdb.readonly {
   271  		frdb.wg.Add(1)
   272  		go func() {
   273  			frdb.freeze(db)
   274  			frdb.wg.Done()
   275  		}()
   276  	}
   277  	return &freezerdb{
   278  		ancientRoot:   ancient,
   279  		KeyValueStore: db,
   280  		AncientStore:  frdb,
   281  	}, nil
   282  }
   283  
   284  // NewMemoryDatabase creates an ephemeral in-memory key-value database without a
   285  // freezer moving immutable chain segments into cold storage.
   286  func NewMemoryDatabase() ethdb.Database {
   287  	return NewDatabase(memorydb.New())
   288  }
   289  
   290  // NewMemoryDatabaseWithCap creates an ephemeral in-memory key-value database
   291  // with an initial starting capacity, but without a freezer moving immutable
   292  // chain segments into cold storage.
   293  func NewMemoryDatabaseWithCap(size int) ethdb.Database {
   294  	return NewDatabase(memorydb.NewWithCap(size))
   295  }
   296  
   297  // NewLevelDBDatabase creates a persistent key-value database without a freezer
   298  // moving immutable chain segments into cold storage.
   299  func NewLevelDBDatabase(file string, cache int, handles int, namespace string, readonly bool) (ethdb.Database, error) {
   300  	db, err := leveldb.New(file, cache, handles, namespace, readonly)
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  	return NewDatabase(db), nil
   305  }
   306  
   307  // NewLevelDBDatabaseWithFreezer creates a persistent key-value database with a
   308  // freezer moving immutable chain segments into cold storage. The passed ancient
   309  // indicates the path of root ancient directory where the chain freezer can be
   310  // opened.
   311  func NewLevelDBDatabaseWithFreezer(file string, cache int, handles int, ancient string, namespace string, readonly bool) (ethdb.Database, error) {
   312  	kvdb, err := leveldb.New(file, cache, handles, namespace, readonly)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	frdb, err := NewDatabaseWithFreezer(kvdb, ancient, namespace, readonly)
   317  	if err != nil {
   318  		kvdb.Close()
   319  		return nil, err
   320  	}
   321  	return frdb, nil
   322  }
   323  
   324  type counter uint64
   325  
   326  func (c counter) String() string {
   327  	return fmt.Sprintf("%d", c)
   328  }
   329  
   330  func (c counter) Percentage(current uint64) string {
   331  	return fmt.Sprintf("%d", current*100/uint64(c))
   332  }
   333  
   334  // stat stores sizes and count for a parameter
   335  type stat struct {
   336  	size  common.StorageSize
   337  	count counter
   338  }
   339  
   340  // Add size to the stat and increase the counter by 1
   341  func (s *stat) Add(size common.StorageSize) {
   342  	s.size += size
   343  	s.count++
   344  }
   345  
   346  func (s *stat) Size() string {
   347  	return s.size.String()
   348  }
   349  
   350  func (s *stat) Count() string {
   351  	return s.count.String()
   352  }
   353  
   354  // InspectDatabase traverses the entire database and checks the size
   355  // of all different categories of data.
   356  func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
   357  	it := db.NewIterator(keyPrefix, keyStart)
   358  	defer it.Release()
   359  
   360  	var (
   361  		count  int64
   362  		start  = time.Now()
   363  		logged = time.Now()
   364  
   365  		// Key-value store statistics
   366  		headers         stat
   367  		bodies          stat
   368  		receipts        stat
   369  		tds             stat
   370  		numHashPairings stat
   371  		hashNumPairings stat
   372  		tries           stat
   373  		codes           stat
   374  		txLookups       stat
   375  		accountSnaps    stat
   376  		storageSnaps    stat
   377  		preimages       stat
   378  		bloomBits       stat
   379  		beaconHeaders   stat
   380  		cliqueSnaps     stat
   381  
   382  		// Ancient store statistics
   383  		ancientHeadersSize  common.StorageSize
   384  		ancientBodiesSize   common.StorageSize
   385  		ancientReceiptsSize common.StorageSize
   386  		ancientTdsSize      common.StorageSize
   387  		ancientHashesSize   common.StorageSize
   388  
   389  		// Les statistic
   390  		chtTrieNodes   stat
   391  		bloomTrieNodes stat
   392  
   393  		// Meta- and unaccounted data
   394  		metadata    stat
   395  		unaccounted stat
   396  
   397  		// Totals
   398  		total common.StorageSize
   399  	)
   400  	// Inspect key-value database first.
   401  	for it.Next() {
   402  		var (
   403  			key  = it.Key()
   404  			size = common.StorageSize(len(key) + len(it.Value()))
   405  		)
   406  		total += size
   407  		switch {
   408  		case bytes.HasPrefix(key, headerPrefix) && len(key) == (len(headerPrefix)+8+common.HashLength):
   409  			headers.Add(size)
   410  		case bytes.HasPrefix(key, blockBodyPrefix) && len(key) == (len(blockBodyPrefix)+8+common.HashLength):
   411  			bodies.Add(size)
   412  		case bytes.HasPrefix(key, blockReceiptsPrefix) && len(key) == (len(blockReceiptsPrefix)+8+common.HashLength):
   413  			receipts.Add(size)
   414  		case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix):
   415  			tds.Add(size)
   416  		case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix):
   417  			numHashPairings.Add(size)
   418  		case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength):
   419  			hashNumPairings.Add(size)
   420  		case len(key) == common.HashLength:
   421  			tries.Add(size)
   422  		case bytes.HasPrefix(key, CodePrefix) && len(key) == len(CodePrefix)+common.HashLength:
   423  			codes.Add(size)
   424  		case bytes.HasPrefix(key, txLookupPrefix) && len(key) == (len(txLookupPrefix)+common.HashLength):
   425  			txLookups.Add(size)
   426  		case bytes.HasPrefix(key, SnapshotAccountPrefix) && len(key) == (len(SnapshotAccountPrefix)+common.HashLength):
   427  			accountSnaps.Add(size)
   428  		case bytes.HasPrefix(key, SnapshotStoragePrefix) && len(key) == (len(SnapshotStoragePrefix)+2*common.HashLength):
   429  			storageSnaps.Add(size)
   430  		case bytes.HasPrefix(key, PreimagePrefix) && len(key) == (len(PreimagePrefix)+common.HashLength):
   431  			preimages.Add(size)
   432  		case bytes.HasPrefix(key, configPrefix) && len(key) == (len(configPrefix)+common.HashLength):
   433  			metadata.Add(size)
   434  		case bytes.HasPrefix(key, genesisPrefix) && len(key) == (len(genesisPrefix)+common.HashLength):
   435  			metadata.Add(size)
   436  		case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength):
   437  			bloomBits.Add(size)
   438  		case bytes.HasPrefix(key, BloomBitsIndexPrefix):
   439  			bloomBits.Add(size)
   440  		case bytes.HasPrefix(key, skeletonHeaderPrefix) && len(key) == (len(skeletonHeaderPrefix)+8):
   441  			beaconHeaders.Add(size)
   442  		case bytes.HasPrefix(key, []byte("clique-")) && len(key) == 7+common.HashLength:
   443  			cliqueSnaps.Add(size)
   444  		case bytes.HasPrefix(key, []byte("cht-")) ||
   445  			bytes.HasPrefix(key, []byte("chtIndexV2-")) ||
   446  			bytes.HasPrefix(key, []byte("chtRootV2-")): // Canonical hash trie
   447  			chtTrieNodes.Add(size)
   448  		case bytes.HasPrefix(key, []byte("blt-")) ||
   449  			bytes.HasPrefix(key, []byte("bltIndex-")) ||
   450  			bytes.HasPrefix(key, []byte("bltRoot-")): // Bloomtrie sub
   451  			bloomTrieNodes.Add(size)
   452  		default:
   453  			var accounted bool
   454  			for _, meta := range [][]byte{
   455  				databaseVersionKey, headHeaderKey, headBlockKey, headFastBlockKey, headFinalizedBlockKey,
   456  				lastPivotKey, fastTrieProgressKey, snapshotDisabledKey, SnapshotRootKey, snapshotJournalKey,
   457  				snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey,
   458  				uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey,
   459  			} {
   460  				if bytes.Equal(key, meta) {
   461  					metadata.Add(size)
   462  					accounted = true
   463  					break
   464  				}
   465  			}
   466  			if !accounted {
   467  				unaccounted.Add(size)
   468  			}
   469  		}
   470  		count++
   471  		if count%1000 == 0 && time.Since(logged) > 8*time.Second {
   472  			log.Info("Inspecting database", "count", count, "elapsed", common.PrettyDuration(time.Since(start)))
   473  			logged = time.Now()
   474  		}
   475  	}
   476  	// Inspect append-only file store then.
   477  	ancientSizes := []*common.StorageSize{&ancientHeadersSize, &ancientBodiesSize, &ancientReceiptsSize, &ancientHashesSize, &ancientTdsSize}
   478  	for i, category := range []string{chainFreezerHeaderTable, chainFreezerBodiesTable, chainFreezerReceiptTable, chainFreezerHashTable, chainFreezerDifficultyTable} {
   479  		if size, err := db.AncientSize(category); err == nil {
   480  			*ancientSizes[i] += common.StorageSize(size)
   481  			total += common.StorageSize(size)
   482  		}
   483  	}
   484  	// Get number of ancient rows inside the freezer
   485  	ancients := counter(0)
   486  	if count, err := db.Ancients(); err == nil {
   487  		ancients = counter(count)
   488  	}
   489  	// Display the database statistic.
   490  	stats := [][]string{
   491  		{"Key-Value store", "Headers", headers.Size(), headers.Count()},
   492  		{"Key-Value store", "Bodies", bodies.Size(), bodies.Count()},
   493  		{"Key-Value store", "Receipt lists", receipts.Size(), receipts.Count()},
   494  		{"Key-Value store", "Difficulties", tds.Size(), tds.Count()},
   495  		{"Key-Value store", "Block number->hash", numHashPairings.Size(), numHashPairings.Count()},
   496  		{"Key-Value store", "Block hash->number", hashNumPairings.Size(), hashNumPairings.Count()},
   497  		{"Key-Value store", "Transaction index", txLookups.Size(), txLookups.Count()},
   498  		{"Key-Value store", "Bloombit index", bloomBits.Size(), bloomBits.Count()},
   499  		{"Key-Value store", "Contract codes", codes.Size(), codes.Count()},
   500  		{"Key-Value store", "Trie nodes", tries.Size(), tries.Count()},
   501  		{"Key-Value store", "Trie preimages", preimages.Size(), preimages.Count()},
   502  		{"Key-Value store", "Account snapshot", accountSnaps.Size(), accountSnaps.Count()},
   503  		{"Key-Value store", "Storage snapshot", storageSnaps.Size(), storageSnaps.Count()},
   504  		{"Key-Value store", "Beacon sync headers", beaconHeaders.Size(), beaconHeaders.Count()},
   505  		{"Key-Value store", "Clique snapshots", cliqueSnaps.Size(), cliqueSnaps.Count()},
   506  		{"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()},
   507  		{"Ancient store", "Headers", ancientHeadersSize.String(), ancients.String()},
   508  		{"Ancient store", "Bodies", ancientBodiesSize.String(), ancients.String()},
   509  		{"Ancient store", "Receipt lists", ancientReceiptsSize.String(), ancients.String()},
   510  		{"Ancient store", "Difficulties", ancientTdsSize.String(), ancients.String()},
   511  		{"Ancient store", "Block number->hash", ancientHashesSize.String(), ancients.String()},
   512  		{"Light client", "CHT trie nodes", chtTrieNodes.Size(), chtTrieNodes.Count()},
   513  		{"Light client", "Bloom trie nodes", bloomTrieNodes.Size(), bloomTrieNodes.Count()},
   514  	}
   515  	table := tablewriter.NewWriter(os.Stdout)
   516  	table.SetHeader([]string{"Database", "Category", "Size", "Items"})
   517  	table.SetFooter([]string{"", "Total", total.String(), " "})
   518  	table.AppendBulk(stats)
   519  	table.Render()
   520  
   521  	if unaccounted.size > 0 {
   522  		log.Error("Database contains unaccounted data", "size", unaccounted.size, "count", unaccounted.count)
   523  	}
   524  
   525  	return nil
   526  }