diff --git a/util/findcheckpoint/config.go b/util/findcheckpoint/config.go new file mode 100644 index 00000000..8a9b9226 --- /dev/null +++ b/util/findcheckpoint/config.go @@ -0,0 +1,99 @@ +// Copyright (c) 2013 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "opensource.conformal.com/go/go-flags" + "os" + "path/filepath" +) + +const ( + minCandidates = 1 + maxCandidates = 20 + defaultNumCandidates = 5 +) + +var ( + defaultDataDir = filepath.Join(btcdHomeDir(), "data") +) + +// config defines the configuration options for findcheckpoint. +// +// See loadConfig for details on the configuration load process. +type config struct { + DataDir string `short:"b" long:"datadir" description:"Location of the btcd data directory"` + NumCandidates int `short:"n" long:"numcandidates" description:"Max num of checkpoint candidates to show {1-20}"` + UseGoOutput bool `short:"g" long:"gooutput" description:"Display the candidates using Go syntax that is ready to insert into the btcchain checkpoint list"` +} + +// btcdHomeDir returns an OS appropriate home directory for btcd. +func btcdHomeDir() string { + // Search for Windows APPDATA first. This won't exist on POSIX OSes. + appData := os.Getenv("APPDATA") + if appData != "" { + return filepath.Join(appData, "btcd") + } + + // Fall back to standard HOME directory that works for most POSIX OSes. + home := os.Getenv("HOME") + if home != "" { + return filepath.Join(home, ".btcd") + } + + // In the worst case, use the current directory. + return "." +} + +// loadConfig initializes and parses the config using command line options. +// +// The configuration proceeds as follows: +// 1) Start with a default config with sane settings +// 2) Pre-parse the command line to check for an alternative config file +// 3) Load configuration file overwriting defaults with any specified options +// 4) Parse CLI options and overwrite/add any specified options +// +// The above results in btcd functioning properly without any config settings +// while still allowing the user to override settings with config files and +// command line options. Command line options always take precedence. +func loadConfig() (*config, []string, error) { + // Default config. + cfg := config{ + DataDir: defaultDataDir, + NumCandidates: defaultNumCandidates, + } + + // Parse command line options. + parser := flags.NewParser(&cfg, flags.Default) + remainingArgs, err := parser.Parse() + if err != nil { + if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp { + parser.WriteHelp(os.Stderr) + } + return nil, nil, err + } + + // Validate the number of candidates. + if cfg.NumCandidates < minCandidates || cfg.NumCandidates > maxCandidates { + str := "%s: The specified number of candidates is out of " + + "range -- parsed [%v]" + err = fmt.Errorf(str, "loadConfig", cfg.NumCandidates) + fmt.Fprintln(os.Stderr, err) + parser.WriteHelp(os.Stderr) + return nil, nil, err + } + + // Append the network type to the data directory so it is "namespaced" + // per network. In addition to the block database, there are other + // pieces of data that are saved to disk such as address manager state. + // All data is specific to a network, so namespacing the data directory + // means each individual piece of serialized data does not have to + // worry about changing names per network and such. + //cfg.DataDir = cleanAndExpandPath(cfg.DataDir) + cfg.DataDir = filepath.Join(cfg.DataDir, "mainnet") + + return &cfg, remainingArgs, nil +} diff --git a/util/findcheckpoint/findcheckpoint.go b/util/findcheckpoint/findcheckpoint.go new file mode 100644 index 00000000..04bb0b21 --- /dev/null +++ b/util/findcheckpoint/findcheckpoint.go @@ -0,0 +1,175 @@ +// Copyright (c) 2013 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "github.com/conformal/btcchain" + "github.com/conformal/btcdb" + _ "github.com/conformal/btcdb/ldb" + _ "github.com/conformal/btcdb/sqlite3" + "github.com/conformal/btcwire" + "os" + "path/filepath" +) + +const blockDbNamePrefix = "blocks" + +var ( + cfg *config +) + +// loadBlockDB opens the block database and returns a handle to it. +func loadBlockDB() (btcdb.Db, error) { + // The database name is based on the database type. + dbType := "leveldb" + dbName := blockDbNamePrefix + "_" + dbType + if dbType == "sqlite" { + dbName = dbName + ".db" + } + dbPath := filepath.Join(cfg.DataDir, dbName) + fmt.Printf("Loading block database from '%s'\n", dbPath) + db, err := btcdb.OpenDB(dbType, dbPath) + if err != nil { + return nil, err + } + return db, nil +} + +// findCandidates searches the chain backwards for checkpoint candidates and +// returns a slice of found candidates, if any. It also stops searching for +// candidates at the last checkpoint that is already hard coded into btcchain +// since there is no point in finding candidates before already existing +// checkpoints. +func findCandidates(db btcdb.Db, latestHash *btcwire.ShaHash) ([]*btcchain.Checkpoint, error) { + // Start with the latest block of the main chain. + block, err := db.FetchBlockBySha(latestHash) + if err != nil { + return nil, err + } + + // Setup chain and get the latest checkpoint. Ignore notifications + // since they aren't needed for this util. + chain := btcchain.New(db, btcwire.MainNet, nil) + latestCheckpoint := chain.LatestCheckpoint() + if latestCheckpoint == nil { + return nil, fmt.Errorf("unable to retrieve latest checkpoint") + } + + // The latest known block must be at least the last known checkpoint + // plus required checkpoint confirmations. + checkpointConfirmations := int64(btcchain.CheckpointConfirmations) + requiredHeight := latestCheckpoint.Height + checkpointConfirmations + if block.Height() < requiredHeight { + return nil, fmt.Errorf("the block database is only at height "+ + "%d which is less than the latest checkpoint height "+ + "of %d plus required confirmations of %d", + block.Height(), latestCheckpoint.Height, + checkpointConfirmations) + } + + // Indeterminate progress setup. + numBlocksToTest := block.Height() - requiredHeight + progressInterval := (numBlocksToTest / 100) + 1 // min 1 + fmt.Print("Searching for candidates") + defer fmt.Println() + + // Loop backwards through the chain to find checkpoint candidates. + var candidates []*btcchain.Checkpoint + numTested := int64(0) + for len(candidates) < cfg.NumCandidates && block.Height() > requiredHeight { + // Display progress. + if numTested%progressInterval == 0 { + fmt.Print(".") + } + + // Determine if this block is a checkpoint candidate. + isCandidate, err := chain.IsCheckpointCandidate(block) + if err != nil { + return nil, err + } + + // All checks passed, so this node seems like a reasonable + // checkpoint candidate. + if isCandidate { + candidateHash, err := block.Sha() + if err != nil { + return nil, err + } + checkpoint := btcchain.Checkpoint{ + Height: block.Height(), + Hash: candidateHash, + } + candidates = append(candidates, &checkpoint) + } + + prevHash := &block.MsgBlock().Header.PrevBlock + block, err = db.FetchBlockBySha(prevHash) + if err != nil { + return nil, err + } + numTested++ + } + return candidates, nil +} + +// showCandidate display a checkpoint candidate using and output format +// determined by the configuration parameters. The Go syntax output +// uses the format the btcchain code expects for checkpoints added to the list. +func showCandidate(candidateNum int, checkpoint *btcchain.Checkpoint) { + if cfg.UseGoOutput { + fmt.Printf("Candidate %d -- {%d, newShaHashFromStr(\"%v\")},\n", + candidateNum, checkpoint.Height, checkpoint.Hash) + return + } + + fmt.Printf("Candidate %d -- Height: %d, Hash: %v\n", candidateNum, + checkpoint.Height, checkpoint.Hash) + +} + +func main() { + // Load configuration and parse command line. + tcfg, _, err := loadConfig() + if err != nil { + return + } + cfg = tcfg + + // Load the block database. + db, err := loadBlockDB() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to load database: %v\n", err) + return + } + defer db.Close() + + // Get the latest block hash and height from the database and report + // status. + latestHash, height, err := db.NewestSha() + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + fmt.Printf("Block database loaded with block height %d\n", height) + + // Find checkpoint candidates. + candidates, err := findCandidates(db, latestHash) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to identify candidates: %v", err) + return + } + + // No candidates. + if len(candidates) == 0 { + fmt.Println("No candidates found.") + return + } + + // Show the candidates. + for i, checkpoint := range candidates { + showCandidate(i+1, checkpoint) + } +}