github.com/valorbit/go-ethereum@v1.9.11-rc4/cmd/geth/chaincmd.go (about)

     1  // Copyright 2015 The go-ethereum Authors
     2  // This file is part of go-ethereum.
     3  //
     4  // go-ethereum is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU 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  // go-ethereum 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 General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU General Public License
    15  // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package main
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strconv"
    26  	"sync/atomic"
    27  	"time"
    28  
    29  	"github.com/valorbit/go-ethereum/cmd/utils"
    30  	"github.com/valorbit/go-ethereum/common"
    31  	"github.com/valorbit/go-ethereum/console"
    32  	"github.com/valorbit/go-ethereum/core"
    33  	"github.com/valorbit/go-ethereum/core/rawdb"
    34  	"github.com/valorbit/go-ethereum/core/state"
    35  	"github.com/valorbit/go-ethereum/core/types"
    36  	"github.com/valorbit/go-ethereum/eth/downloader"
    37  	"github.com/valorbit/go-ethereum/event"
    38  	"github.com/valorbit/go-ethereum/log"
    39  	"github.com/valorbit/go-ethereum/trie"
    40  	"gopkg.in/urfave/cli.v1"
    41  )
    42  
    43  var (
    44  	initCommand = cli.Command{
    45  		Action:    utils.MigrateFlags(initGenesis),
    46  		Name:      "init",
    47  		Usage:     "Bootstrap and initialize a new genesis block",
    48  		ArgsUsage: "<genesisPath>",
    49  		Flags: []cli.Flag{
    50  			utils.DataDirFlag,
    51  		},
    52  		Category: "BLOCKCHAIN COMMANDS",
    53  		Description: `
    54  The init command initializes a new genesis block and definition for the network.
    55  This is a destructive action and changes the network in which you will be
    56  participating.
    57  
    58  It expects the genesis file as argument.`,
    59  	}
    60  	dumpGenesisCommand = cli.Command{
    61  		Action:    utils.MigrateFlags(dumpGenesis),
    62  		Name:      "dumpgenesis",
    63  		Usage:     "Dumps genesis block JSON configuration to stdout",
    64  		ArgsUsage: "",
    65  		Flags: []cli.Flag{
    66  			utils.DataDirFlag,
    67  		},
    68  		Category: "BLOCKCHAIN COMMANDS",
    69  		Description: `
    70  The dumpgenesis command dumps the genesis block configuration in JSON format to stdout.`,
    71  	}
    72  	importCommand = cli.Command{
    73  		Action:    utils.MigrateFlags(importChain),
    74  		Name:      "import",
    75  		Usage:     "Import a blockchain file",
    76  		ArgsUsage: "<filename> (<filename 2> ... <filename N>) ",
    77  		Flags: []cli.Flag{
    78  			utils.DataDirFlag,
    79  			utils.CacheFlag,
    80  			utils.SyncModeFlag,
    81  			utils.GCModeFlag,
    82  			utils.CacheDatabaseFlag,
    83  			utils.CacheGCFlag,
    84  		},
    85  		Category: "BLOCKCHAIN COMMANDS",
    86  		Description: `
    87  The import command imports blocks from an RLP-encoded form. The form can be one file
    88  with several RLP-encoded blocks, or several files can be used.
    89  
    90  If only one file is used, import error will result in failure. If several files are used,
    91  processing will proceed even if an individual RLP-file import failure occurs.`,
    92  	}
    93  	exportCommand = cli.Command{
    94  		Action:    utils.MigrateFlags(exportChain),
    95  		Name:      "export",
    96  		Usage:     "Export blockchain into file",
    97  		ArgsUsage: "<filename> [<blockNumFirst> <blockNumLast>]",
    98  		Flags: []cli.Flag{
    99  			utils.DataDirFlag,
   100  			utils.CacheFlag,
   101  			utils.SyncModeFlag,
   102  		},
   103  		Category: "BLOCKCHAIN COMMANDS",
   104  		Description: `
   105  Requires a first argument of the file to write to.
   106  Optional second and third arguments control the first and
   107  last block to write. In this mode, the file will be appended
   108  if already existing. If the file ends with .gz, the output will
   109  be gzipped.`,
   110  	}
   111  	importPreimagesCommand = cli.Command{
   112  		Action:    utils.MigrateFlags(importPreimages),
   113  		Name:      "import-preimages",
   114  		Usage:     "Import the preimage database from an RLP stream",
   115  		ArgsUsage: "<datafile>",
   116  		Flags: []cli.Flag{
   117  			utils.DataDirFlag,
   118  			utils.CacheFlag,
   119  			utils.SyncModeFlag,
   120  		},
   121  		Category: "BLOCKCHAIN COMMANDS",
   122  		Description: `
   123  	The import-preimages command imports hash preimages from an RLP encoded stream.`,
   124  	}
   125  	exportPreimagesCommand = cli.Command{
   126  		Action:    utils.MigrateFlags(exportPreimages),
   127  		Name:      "export-preimages",
   128  		Usage:     "Export the preimage database into an RLP stream",
   129  		ArgsUsage: "<dumpfile>",
   130  		Flags: []cli.Flag{
   131  			utils.DataDirFlag,
   132  			utils.CacheFlag,
   133  			utils.SyncModeFlag,
   134  		},
   135  		Category: "BLOCKCHAIN COMMANDS",
   136  		Description: `
   137  The export-preimages command export hash preimages to an RLP encoded stream`,
   138  	}
   139  	copydbCommand = cli.Command{
   140  		Action:    utils.MigrateFlags(copyDb),
   141  		Name:      "copydb",
   142  		Usage:     "Create a local chain from a target chaindata folder",
   143  		ArgsUsage: "<sourceChaindataDir>",
   144  		Flags: []cli.Flag{
   145  			utils.DataDirFlag,
   146  			utils.CacheFlag,
   147  			utils.SyncModeFlag,
   148  			utils.FakePoWFlag,
   149  			utils.TestnetFlag,
   150  			utils.RinkebyFlag,
   151  		},
   152  		Category: "BLOCKCHAIN COMMANDS",
   153  		Description: `
   154  The first argument must be the directory containing the blockchain to download from`,
   155  	}
   156  	removedbCommand = cli.Command{
   157  		Action:    utils.MigrateFlags(removeDB),
   158  		Name:      "removedb",
   159  		Usage:     "Remove blockchain and state databases",
   160  		ArgsUsage: " ",
   161  		Flags: []cli.Flag{
   162  			utils.DataDirFlag,
   163  		},
   164  		Category: "BLOCKCHAIN COMMANDS",
   165  		Description: `
   166  Remove blockchain and state databases`,
   167  	}
   168  	dumpCommand = cli.Command{
   169  		Action:    utils.MigrateFlags(dump),
   170  		Name:      "dump",
   171  		Usage:     "Dump a specific block from storage",
   172  		ArgsUsage: "[<blockHash> | <blockNum>]...",
   173  		Flags: []cli.Flag{
   174  			utils.DataDirFlag,
   175  			utils.CacheFlag,
   176  			utils.SyncModeFlag,
   177  			utils.IterativeOutputFlag,
   178  			utils.ExcludeCodeFlag,
   179  			utils.ExcludeStorageFlag,
   180  			utils.IncludeIncompletesFlag,
   181  		},
   182  		Category: "BLOCKCHAIN COMMANDS",
   183  		Description: `
   184  The arguments are interpreted as block numbers or hashes.
   185  Use "ethereum dump 0" to dump the genesis block.`,
   186  	}
   187  	inspectCommand = cli.Command{
   188  		Action:    utils.MigrateFlags(inspect),
   189  		Name:      "inspect",
   190  		Usage:     "Inspect the storage size for each type of data in the database",
   191  		ArgsUsage: " ",
   192  		Flags: []cli.Flag{
   193  			utils.DataDirFlag,
   194  			utils.AncientFlag,
   195  			utils.CacheFlag,
   196  			utils.TestnetFlag,
   197  			utils.RinkebyFlag,
   198  			utils.GoerliFlag,
   199  			utils.ValorbitFlag,
   200  			utils.GranvilleFlag,
   201  			utils.SyncModeFlag,
   202  		},
   203  		Category: "BLOCKCHAIN COMMANDS",
   204  	}
   205  )
   206  
   207  // initGenesis will initialise the given JSON format genesis file and writes it as
   208  // the zero'd block (i.e. genesis) or will fail hard if it can't succeed.
   209  func initGenesis(ctx *cli.Context) error {
   210  	// Make sure we have a valid genesis JSON
   211  	genesisPath := ctx.Args().First()
   212  	if len(genesisPath) == 0 {
   213  		utils.Fatalf("Must supply path to genesis JSON file")
   214  	}
   215  	file, err := os.Open(genesisPath)
   216  	if err != nil {
   217  		utils.Fatalf("Failed to read genesis file: %v", err)
   218  	}
   219  	defer file.Close()
   220  
   221  	genesis := new(core.Genesis)
   222  	if err := json.NewDecoder(file).Decode(genesis); err != nil {
   223  		utils.Fatalf("invalid genesis file: %v", err)
   224  	}
   225  	// Open an initialise both full and light databases
   226  	stack := makeFullNode(ctx)
   227  	defer stack.Close()
   228  
   229  	for _, name := range []string{"chaindata", "lightchaindata"} {
   230  		chaindb, err := stack.OpenDatabase(name, 0, 0, "")
   231  		if err != nil {
   232  			utils.Fatalf("Failed to open database: %v", err)
   233  		}
   234  		_, hash, err := core.SetupGenesisBlock(chaindb, genesis)
   235  		if err != nil {
   236  			utils.Fatalf("Failed to write genesis block: %v", err)
   237  		}
   238  		chaindb.Close()
   239  		log.Info("Successfully wrote genesis state", "database", name, "hash", hash)
   240  	}
   241  	return nil
   242  }
   243  
   244  func dumpGenesis(ctx *cli.Context) error {
   245  	genesis := utils.MakeGenesis(ctx)
   246  	if genesis == nil {
   247  		genesis = core.DefaultGenesisBlock()
   248  	}
   249  	if err := json.NewEncoder(os.Stdout).Encode(genesis); err != nil {
   250  		utils.Fatalf("could not encode genesis")
   251  	}
   252  	return nil
   253  }
   254  
   255  func importChain(ctx *cli.Context) error {
   256  	if len(ctx.Args()) < 1 {
   257  		utils.Fatalf("This command requires an argument.")
   258  	}
   259  	stack := makeFullNode(ctx)
   260  	defer stack.Close()
   261  
   262  	chain, db := utils.MakeChain(ctx, stack)
   263  	defer db.Close()
   264  
   265  	// Start periodically gathering memory profiles
   266  	var peakMemAlloc, peakMemSys uint64
   267  	go func() {
   268  		stats := new(runtime.MemStats)
   269  		for {
   270  			runtime.ReadMemStats(stats)
   271  			if atomic.LoadUint64(&peakMemAlloc) < stats.Alloc {
   272  				atomic.StoreUint64(&peakMemAlloc, stats.Alloc)
   273  			}
   274  			if atomic.LoadUint64(&peakMemSys) < stats.Sys {
   275  				atomic.StoreUint64(&peakMemSys, stats.Sys)
   276  			}
   277  			time.Sleep(5 * time.Second)
   278  		}
   279  	}()
   280  	// Import the chain
   281  	start := time.Now()
   282  
   283  	if len(ctx.Args()) == 1 {
   284  		if err := utils.ImportChain(chain, ctx.Args().First()); err != nil {
   285  			log.Error("Import error", "err", err)
   286  		}
   287  	} else {
   288  		for _, arg := range ctx.Args() {
   289  			if err := utils.ImportChain(chain, arg); err != nil {
   290  				log.Error("Import error", "file", arg, "err", err)
   291  			}
   292  		}
   293  	}
   294  	chain.Stop()
   295  	fmt.Printf("Import done in %v.\n\n", time.Since(start))
   296  
   297  	// Output pre-compaction stats mostly to see the import trashing
   298  	stats, err := db.Stat("leveldb.stats")
   299  	if err != nil {
   300  		utils.Fatalf("Failed to read database stats: %v", err)
   301  	}
   302  	fmt.Println(stats)
   303  
   304  	ioStats, err := db.Stat("leveldb.iostats")
   305  	if err != nil {
   306  		utils.Fatalf("Failed to read database iostats: %v", err)
   307  	}
   308  	fmt.Println(ioStats)
   309  
   310  	// Print the memory statistics used by the importing
   311  	mem := new(runtime.MemStats)
   312  	runtime.ReadMemStats(mem)
   313  
   314  	fmt.Printf("Object memory: %.3f MB current, %.3f MB peak\n", float64(mem.Alloc)/1024/1024, float64(atomic.LoadUint64(&peakMemAlloc))/1024/1024)
   315  	fmt.Printf("System memory: %.3f MB current, %.3f MB peak\n", float64(mem.Sys)/1024/1024, float64(atomic.LoadUint64(&peakMemSys))/1024/1024)
   316  	fmt.Printf("Allocations:   %.3f million\n", float64(mem.Mallocs)/1000000)
   317  	fmt.Printf("GC pause:      %v\n\n", time.Duration(mem.PauseTotalNs))
   318  
   319  	if ctx.GlobalBool(utils.NoCompactionFlag.Name) {
   320  		return nil
   321  	}
   322  
   323  	// Compact the entire database to more accurately measure disk io and print the stats
   324  	start = time.Now()
   325  	fmt.Println("Compacting entire database...")
   326  	if err = db.Compact(nil, nil); err != nil {
   327  		utils.Fatalf("Compaction failed: %v", err)
   328  	}
   329  	fmt.Printf("Compaction done in %v.\n\n", time.Since(start))
   330  
   331  	stats, err = db.Stat("leveldb.stats")
   332  	if err != nil {
   333  		utils.Fatalf("Failed to read database stats: %v", err)
   334  	}
   335  	fmt.Println(stats)
   336  
   337  	ioStats, err = db.Stat("leveldb.iostats")
   338  	if err != nil {
   339  		utils.Fatalf("Failed to read database iostats: %v", err)
   340  	}
   341  	fmt.Println(ioStats)
   342  	return nil
   343  }
   344  
   345  func exportChain(ctx *cli.Context) error {
   346  	if len(ctx.Args()) < 1 {
   347  		utils.Fatalf("This command requires an argument.")
   348  	}
   349  	stack := makeFullNode(ctx)
   350  	defer stack.Close()
   351  
   352  	chain, _ := utils.MakeChain(ctx, stack)
   353  	start := time.Now()
   354  
   355  	var err error
   356  	fp := ctx.Args().First()
   357  	if len(ctx.Args()) < 3 {
   358  		err = utils.ExportChain(chain, fp)
   359  	} else {
   360  		// This can be improved to allow for numbers larger than 9223372036854775807
   361  		first, ferr := strconv.ParseInt(ctx.Args().Get(1), 10, 64)
   362  		last, lerr := strconv.ParseInt(ctx.Args().Get(2), 10, 64)
   363  		if ferr != nil || lerr != nil {
   364  			utils.Fatalf("Export error in parsing parameters: block number not an integer\n")
   365  		}
   366  		if first < 0 || last < 0 {
   367  			utils.Fatalf("Export error: block number must be greater than 0\n")
   368  		}
   369  		err = utils.ExportAppendChain(chain, fp, uint64(first), uint64(last))
   370  	}
   371  
   372  	if err != nil {
   373  		utils.Fatalf("Export error: %v\n", err)
   374  	}
   375  	fmt.Printf("Export done in %v\n", time.Since(start))
   376  	return nil
   377  }
   378  
   379  // importPreimages imports preimage data from the specified file.
   380  func importPreimages(ctx *cli.Context) error {
   381  	if len(ctx.Args()) < 1 {
   382  		utils.Fatalf("This command requires an argument.")
   383  	}
   384  	stack := makeFullNode(ctx)
   385  	defer stack.Close()
   386  
   387  	db := utils.MakeChainDatabase(ctx, stack)
   388  	start := time.Now()
   389  
   390  	if err := utils.ImportPreimages(db, ctx.Args().First()); err != nil {
   391  		utils.Fatalf("Import error: %v\n", err)
   392  	}
   393  	fmt.Printf("Import done in %v\n", time.Since(start))
   394  	return nil
   395  }
   396  
   397  // exportPreimages dumps the preimage data to specified json file in streaming way.
   398  func exportPreimages(ctx *cli.Context) error {
   399  	if len(ctx.Args()) < 1 {
   400  		utils.Fatalf("This command requires an argument.")
   401  	}
   402  	stack := makeFullNode(ctx)
   403  	defer stack.Close()
   404  
   405  	db := utils.MakeChainDatabase(ctx, stack)
   406  	start := time.Now()
   407  
   408  	if err := utils.ExportPreimages(db, ctx.Args().First()); err != nil {
   409  		utils.Fatalf("Export error: %v\n", err)
   410  	}
   411  	fmt.Printf("Export done in %v\n", time.Since(start))
   412  	return nil
   413  }
   414  
   415  func copyDb(ctx *cli.Context) error {
   416  	// Ensure we have a source chain directory to copy
   417  	if len(ctx.Args()) < 1 {
   418  		utils.Fatalf("Source chaindata directory path argument missing")
   419  	}
   420  	if len(ctx.Args()) < 2 {
   421  		utils.Fatalf("Source ancient chain directory path argument missing")
   422  	}
   423  	// Initialize a new chain for the running node to sync into
   424  	stack := makeFullNode(ctx)
   425  	defer stack.Close()
   426  
   427  	chain, chainDb := utils.MakeChain(ctx, stack)
   428  	syncMode := *utils.GlobalTextMarshaler(ctx, utils.SyncModeFlag.Name).(*downloader.SyncMode)
   429  
   430  	var syncBloom *trie.SyncBloom
   431  	if syncMode == downloader.FastSync {
   432  		syncBloom = trie.NewSyncBloom(uint64(ctx.GlobalInt(utils.CacheFlag.Name)/2), chainDb)
   433  	}
   434  	dl := downloader.New(0, chainDb, syncBloom, new(event.TypeMux), chain, nil, nil)
   435  
   436  	// Create a source peer to satisfy downloader requests from
   437  	db, err := rawdb.NewLevelDBDatabaseWithFreezer(ctx.Args().First(), ctx.GlobalInt(utils.CacheFlag.Name)/2, 256, ctx.Args().Get(1), "")
   438  	if err != nil {
   439  		return err
   440  	}
   441  	hc, err := core.NewHeaderChain(db, chain.Config(), chain.Engine(), func() bool { return false })
   442  	if err != nil {
   443  		return err
   444  	}
   445  	peer := downloader.NewFakePeer("local", db, hc, dl)
   446  	if err = dl.RegisterPeer("local", 63, peer); err != nil {
   447  		return err
   448  	}
   449  	// Synchronise with the simulated peer
   450  	start := time.Now()
   451  
   452  	currentHeader := hc.CurrentHeader()
   453  	if err = dl.Synchronise("local", currentHeader.Hash(), hc.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64()), syncMode); err != nil {
   454  		return err
   455  	}
   456  	for dl.Synchronising() {
   457  		time.Sleep(10 * time.Millisecond)
   458  	}
   459  	fmt.Printf("Database copy done in %v\n", time.Since(start))
   460  
   461  	// Compact the entire database to remove any sync overhead
   462  	start = time.Now()
   463  	fmt.Println("Compacting entire database...")
   464  	if err = db.Compact(nil, nil); err != nil {
   465  		utils.Fatalf("Compaction failed: %v", err)
   466  	}
   467  	fmt.Printf("Compaction done in %v.\n\n", time.Since(start))
   468  	return nil
   469  }
   470  
   471  func removeDB(ctx *cli.Context) error {
   472  	stack, config := makeConfigNode(ctx)
   473  
   474  	// Remove the full node state database
   475  	path := stack.ResolvePath("chaindata")
   476  	if common.FileExist(path) {
   477  		confirmAndRemoveDB(path, "full node state database")
   478  	} else {
   479  		log.Info("Full node state database missing", "path", path)
   480  	}
   481  	// Remove the full node ancient database
   482  	path = config.Eth.DatabaseFreezer
   483  	switch {
   484  	case path == "":
   485  		path = filepath.Join(stack.ResolvePath("chaindata"), "ancient")
   486  	case !filepath.IsAbs(path):
   487  		path = config.Node.ResolvePath(path)
   488  	}
   489  	if common.FileExist(path) {
   490  		confirmAndRemoveDB(path, "full node ancient database")
   491  	} else {
   492  		log.Info("Full node ancient database missing", "path", path)
   493  	}
   494  	// Remove the light node database
   495  	path = stack.ResolvePath("lightchaindata")
   496  	if common.FileExist(path) {
   497  		confirmAndRemoveDB(path, "light node database")
   498  	} else {
   499  		log.Info("Light node database missing", "path", path)
   500  	}
   501  	return nil
   502  }
   503  
   504  // confirmAndRemoveDB prompts the user for a last confirmation and removes the
   505  // folder if accepted.
   506  func confirmAndRemoveDB(database string, kind string) {
   507  	confirm, err := console.Stdin.PromptConfirm(fmt.Sprintf("Remove %s (%s)?", kind, database))
   508  	switch {
   509  	case err != nil:
   510  		utils.Fatalf("%v", err)
   511  	case !confirm:
   512  		log.Info("Database deletion skipped", "path", database)
   513  	default:
   514  		start := time.Now()
   515  		filepath.Walk(database, func(path string, info os.FileInfo, err error) error {
   516  			// If we're at the top level folder, recurse into
   517  			if path == database {
   518  				return nil
   519  			}
   520  			// Delete all the files, but not subfolders
   521  			if !info.IsDir() {
   522  				os.Remove(path)
   523  				return nil
   524  			}
   525  			return filepath.SkipDir
   526  		})
   527  		log.Info("Database successfully deleted", "path", database, "elapsed", common.PrettyDuration(time.Since(start)))
   528  	}
   529  }
   530  
   531  func dump(ctx *cli.Context) error {
   532  	stack := makeFullNode(ctx)
   533  	defer stack.Close()
   534  
   535  	chain, chainDb := utils.MakeChain(ctx, stack)
   536  	defer chainDb.Close()
   537  	for _, arg := range ctx.Args() {
   538  		var block *types.Block
   539  		if hashish(arg) {
   540  			block = chain.GetBlockByHash(common.HexToHash(arg))
   541  		} else {
   542  			num, _ := strconv.Atoi(arg)
   543  			block = chain.GetBlockByNumber(uint64(num))
   544  		}
   545  		if block == nil {
   546  			fmt.Println("{}")
   547  			utils.Fatalf("block not found")
   548  		} else {
   549  			state, err := state.New(block.Root(), state.NewDatabase(chainDb))
   550  			if err != nil {
   551  				utils.Fatalf("could not create new state: %v", err)
   552  			}
   553  			excludeCode := ctx.Bool(utils.ExcludeCodeFlag.Name)
   554  			excludeStorage := ctx.Bool(utils.ExcludeStorageFlag.Name)
   555  			includeMissing := ctx.Bool(utils.IncludeIncompletesFlag.Name)
   556  			if ctx.Bool(utils.IterativeOutputFlag.Name) {
   557  				state.IterativeDump(excludeCode, excludeStorage, !includeMissing, json.NewEncoder(os.Stdout))
   558  			} else {
   559  				if includeMissing {
   560  					fmt.Printf("If you want to include accounts with missing preimages, you need iterative output, since" +
   561  						" otherwise the accounts will overwrite each other in the resulting mapping.")
   562  				}
   563  				fmt.Printf("%v %s\n", includeMissing, state.Dump(excludeCode, excludeStorage, false))
   564  			}
   565  		}
   566  	}
   567  	return nil
   568  }
   569  
   570  func inspect(ctx *cli.Context) error {
   571  	node, _ := makeConfigNode(ctx)
   572  	defer node.Close()
   573  
   574  	_, chainDb := utils.MakeChain(ctx, node)
   575  	defer chainDb.Close()
   576  
   577  	return rawdb.InspectDatabase(chainDb)
   578  }
   579  
   580  // hashish returns true for strings that look like hashes.
   581  func hashish(x string) bool {
   582  	_, err := strconv.Atoi(x)
   583  	return err != nil
   584  }