diff --git a/cmd/checkdevpremine/README.md b/cmd/checkdevpremine/README.md new file mode 100644 index 00000000..5e025404 --- /dev/null +++ b/cmd/checkdevpremine/README.md @@ -0,0 +1,75 @@ +checkdevpremine +=============== + +The checkdevpremine utility allows transactions to be tested whether or not they +have inputs that trace back to the original dev premine coins. + +It works by using the dcrd RPC server to request all of the relevant input +transactions all the way back to the coinbase output that orignated the coins. +Those coinbase outpoints are then compiled into a list and compared against the +original dev premine outpoints. This is also known as checking for taint. + +The utility only accepts one parameter which can either be a single transaction +hash or a JSON-array of transaction hashes in order to facilite checking +multiple at once. That parameter may also be a single dash, `-`, in order to +indicate it should be read from stdin. + +Any outpoints which are found to be part of the original dev premine will be +listed unless the `--quiet` flag is provided to suppress the output. + +In addition, in order to facilitate programmatic access the tool returns the +following codes to the Operating System: + +|Return Code|Description| +|---|---| +|0|The transaction(s) do _NOT_ have any inputs which trace back to the dev premine coins| +|1|One of more of the transactions _DO_ have at least one input which traces back to the dev premine coins| +|2|Some type of error such as inability to talk to the RPC server occurred| + +These codes in addition with the `--quiet` flag allow a fully automated check +with no visible output. + +## Configuring + +In order to connect and authenticate to the dcrd RPC server, the `--rpcuser` and +`--rpcpass` options must be specified. These can be placed into a config file +named `checkdevpremine.conf` at the location shown in the help output of the +utility (`checkdevpremine -h`). + +Config file example: + +``` +rpcuser=your_dcrd_RPC_server_username +rpcpass=your_dcrd_RPC_server_password +``` + +## Example Usage + +Checking a single transaction with visible output: +```bash +$ checkdevpremine 25afd7d33ceb8698f5d81eb7ee14a7532419ce1e1c65cc5032e37696f26c5cac +``` + +Checking for multiple transactions with visible output: +```bash +$ checkdevpremine "[\"25afd7d33ceb8698f5d81eb7ee14a7532419ce1e1c65cc5032e37696f26c5cac\", \"b1527b63c7a76ea28e57604082bec0e8195cd7a33bd10c68296a45bce77bd2db\"]" +``` + +Mixing tools in order to check all of the transactions in the latest block using +[jq](https://stedolan.github.io/jq/) to extract the transaction hashes and feed +them to `checkdevpremine` via stdin along with `--quiet` to suppress output: +```bash +$ dcrctl getbestblockhash | dcrctl getblock - | jq -c .tx | checkdevpremine --quiet -; echo $? +0 +``` + +## Installation and Updating + +```bash +$ go get -u github.com/decred/dcrd/cmd/checkdevpremine +``` + +## License + +The checkdevpremine utility is licensed under the [copyfree](http://copyfree.org) +ISC License. diff --git a/cmd/checkdevpremine/config.go b/cmd/checkdevpremine/config.go new file mode 100644 index 00000000..6a36a74c --- /dev/null +++ b/cmd/checkdevpremine/config.go @@ -0,0 +1,142 @@ +// Copyright (c) 2016 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "net" + "os" + "path/filepath" + "strings" + + "github.com/decred/dcrutil" + + flags "github.com/btcsuite/go-flags" +) + +var ( + dcrdHomeDir = dcrutil.AppDataDir("dcrd", false) + appHomeDir = dcrutil.AppDataDir("checkdevpremine", false) + defaultConfigFile = filepath.Join(appHomeDir, "checkdevpremine.conf") + defaultRPCServer = "localhost" + defaultRPCCertFile = filepath.Join(dcrdHomeDir, "rpc.cert") +) + +// config defines the configuration options for dcrctl. +// +// See loadConfig for details on the configuration load process. +type config struct { + ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` + RPCUser string `short:"u" long:"rpcuser" description:"RPC username"` + RPCPassword string `short:"P" long:"rpcpass" default-mask:"-" description:"RPC password"` + RPCServer string `short:"s" long:"rpcserver" description:"RPC server to connect to"` + RPCCert string `short:"c" long:"rpccert" description:"RPC server certificate chain for validation"` + NoTLS bool `long:"notls" description:"Disable TLS"` + TLSSkipVerify bool `long:"skipverify" description:"Do not verify tls certificates (not recommended!)"` + Quiet bool `long:"quiet" description:"Do not print any found outpoints which can be useful if only relying on the return code"` +} + +// normalizeAddress returns addr with the default port appended if there is not +// already a port specified. +func normalizeAddress(addr string) string { + _, _, err := net.SplitHostPort(addr) + if err != nil { + return net.JoinHostPort(addr, "9109") + } + return addr +} + +// cleanAndExpandPath expands environement variables and leading ~ in the +// passed path, cleans the result, and returns it. +func cleanAndExpandPath(path string) string { + // Expand initial ~ to OS specific home directory. + if strings.HasPrefix(path, "~") { + homeDir := filepath.Dir(appHomeDir) + path = strings.Replace(path, "~", homeDir, 1) + } + + // NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%, + // but they variables can still be expanded via POSIX-style $VARIABLE. + return filepath.Clean(os.ExpandEnv(path)) +} + +// loadConfig initializes and parses the config using a config file and 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 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{ + ConfigFile: defaultConfigFile, + RPCServer: defaultRPCServer, + RPCCert: defaultRPCCertFile, + } + + // Create the home directory if it doesn't already exist. + err := os.MkdirAll(dcrdHomeDir, 0700) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(-1) + } + + // Pre-parse the command line options to see if an alternative config + // file, the version flag, or the list commands flag was specified. Any + // errors aside from the help message error can be ignored here since + // they will be caught by the final parse below. + preCfg := cfg + preParser := flags.NewParser(&preCfg, flags.HelpFlag) + _, err = preParser.Parse() + if err != nil { + if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { + fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, "The special parameter `-` "+ + "indicates that a parameter should be read "+ + "from the\nnext unread line from standard "+ + "input.") + return nil, nil, err + } + } + + // Load additional config from file. + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + usageMessage := fmt.Sprintf("Use %s -h to show options", appName) + parser := flags.NewParser(&cfg, flags.Default) + err = flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile) + if err != nil { + if _, ok := err.(*os.PathError); !ok { + fmt.Fprintf(os.Stderr, "Error parsing config file: %v\n", + err) + fmt.Fprintln(os.Stderr, usageMessage) + return nil, nil, err + } + } + + // Parse command line options again to ensure they take precedence. + remainingArgs, err := parser.Parse() + if err != nil { + if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp { + fmt.Fprintln(os.Stderr, usageMessage) + } + return nil, nil, err + } + + // Handle environment variable expansion in the RPC certificate path. + cfg.RPCCert = cleanAndExpandPath(cfg.RPCCert) + + // Add default port to RPC server if needed. + cfg.RPCServer = normalizeAddress(cfg.RPCServer) + + return &cfg, remainingArgs, nil +} diff --git a/cmd/checkdevpremine/main.go b/cmd/checkdevpremine/main.go new file mode 100644 index 00000000..40a56bf0 --- /dev/null +++ b/cmd/checkdevpremine/main.go @@ -0,0 +1,276 @@ +// Copyright (c) 2016 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/decred/dcrd/blockchain" + "github.com/decred/dcrd/blockchain/stake" + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrrpcclient" +) + +// Codes that are returned to the operating system. +const ( + rcNoDevPremineInputs = 0 + rcDevPremineInputs = 1 + rcError = 2 +) + +const ( + // devCoinMaxIndex is the final index in the block 1 premine transaction + // that involves the original developer premine coins. All coins after + // this index are part of the airdrop. + devCoinMaxIndex = 173 +) + +var ( + // premineTxHash is the hash of the transaction in block one which + // creates the premine coins. + premineTxHash = newHashFromStr("5e29cdb355b3fc7e76c98a9983cd44324b3efdd7815c866e33f6c72292cb8be6") +) + +// newHashFromStr converts the passed big-endian hex string into a +// chainhash.Hash. It only differs from the one available in chainhash in that +// it panics on an error since it will only (and must only) be called with +// hard-coded, and therefore known good, hashes. +func newHashFromStr(hexStr string) *chainhash.Hash { + hash, err := chainhash.NewHashFromStr(hexStr) + if err != nil { + panic(err) + } + return hash +} + +// usage displays the general usage when the help flag is not displayed and +// and an invalid command was specified. The commandUsage function is used +// instead when a valid command was specified. +func usage(errorMessage string) { + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + fmt.Fprintln(os.Stderr, errorMessage) + fmt.Fprintln(os.Stderr, "Usage:") + fmt.Fprintf(os.Stderr, " %s [OPTIONS] \n\n", + appName) + fmt.Fprintln(os.Stderr, "Specify -h to show available options") +} + +// isDevPremineOut return whether or not the provided outpoint is one of the +// original dev premine coins. +func isDevPremineOut(out wire.OutPoint) bool { + return out.Hash.IsEqual(premineTxHash) && out.Index <= devCoinMaxIndex && + out.Tree == 0 +} + +// traceDevPremineOuts returns a list of outpoints that are part of the dev +// premine coins and are ancestors of the inputs to the passed transaction hash. +func traceDevPremineOuts(client *dcrrpcclient.Client, txHash *chainhash.Hash) ([]wire.OutPoint, error) { + // Trace the lineage of all inputs to the provided transaction back to + // the coinbase outputs that generated them and add those outpoints to + // a list. Also, keep track of all of the processed transactions in + // order to avoid processing duplicates. + knownCoinbases := make(map[chainhash.Hash]struct{}) + processedHashes := make(map[chainhash.Hash]struct{}) + coinbaseOuts := make([]wire.OutPoint, 0, 10) + processOuts := []wire.OutPoint{{Hash: *txHash}} + for len(processOuts) > 0 { + // Grab the first outpoint to process and skip it if it has + // already been traced. + outpoint := processOuts[0] + processOuts = processOuts[1:] + if _, exists := processedHashes[outpoint.Hash]; exists { + if _, exists := knownCoinbases[outpoint.Hash]; exists { + coinbaseOuts = append(coinbaseOuts, outpoint) + } + continue + } + processedHashes[outpoint.Hash] = struct{}{} + + // Request the transaction for the outpoint from the server. + tx, err := client.GetRawTransaction(&outpoint.Hash) + if err != nil { + return nil, fmt.Errorf("failed to get transaction %v: %v", + &outpoint.Hash, err) + } + + // Add the outpoint to the coinbase outputs list when it is part + // of a coinbase transaction. Also, keep track of the fact the + // transaction is a coinbase to use when avoiding duplicate + // checks. + if blockchain.IsCoinBase(tx) { + knownCoinbases[outpoint.Hash] = struct{}{} + coinbaseOuts = append(coinbaseOuts, outpoint) + continue + } + + // Add the inputs to the transaction to the list of transactions + // to load and continue tracing. + // + // However, skip the first input to stake generation txns since + // they are creating new coins. The remaining inputs to a + // stake generation transaction still need to be traced since + // they represent the coins that purchased the ticket. + txIns := tx.MsgTx().TxIn + isSSGen, _ := stake.IsSSGen(tx) + if isSSGen { + txIns = txIns[1:] + } + for _, txIn := range txIns { + processOuts = append(processOuts, txIn.PreviousOutPoint) + } + } + + // Add any of the outputs that are dev premine outputs to a list. + var devPremineOuts []wire.OutPoint + for _, coinbaseOut := range coinbaseOuts { + if isDevPremineOut(coinbaseOut) { + devPremineOuts = append(devPremineOuts, coinbaseOut) + } + } + + return devPremineOuts, nil +} + +// realMain is the real main function for the utility. It is necessary to work +// around the fact that deferred functions do not run when os.Exit() is called. +func realMain() int { + // Load configuration and parse command line. + cfg, args, err := loadConfig() + if err != nil { + return rcError + } + + // Ensure the user specified a single argument. + if len(args) < 1 { + usage("Transaction hash not specified") + return rcError + } + if len(args) > 1 { + usage("Too many arguments specified") + return rcError + } + + // Read the argument from a stdin pipe when it is '-'. + arg0 := args[0] + if arg0 == "-" { + bio := bufio.NewReader(os.Stdin) + param, err := bio.ReadString('\n') + if err != nil && err != io.EOF { + fmt.Fprintf(os.Stderr, "Failed to read data from "+ + "stdin: %v\n", err) + return rcError + } + if err == io.EOF && len(param) == 0 { + fmt.Fprintln(os.Stderr, "Not enough lines provided on "+ + "stdin") + return rcError + } + arg0 = param + } + arg0 = strings.TrimSpace(arg0) + + // Attempt to unmarshal the parameter as a JSON array of strings if it + // looks like JSON input. This allows multiple transactions to be + // specified via the argument. Treat the argument as a single hash if + // it fails to unmarshal. + var txHashes []*chainhash.Hash + if strings.Contains(arg0, "[") && strings.Contains(arg0, "]") { + var txHashStrs []string + if err := json.Unmarshal([]byte(arg0), &txHashStrs); err != nil { + fmt.Fprintf(os.Stderr, "Failed to unmarshal JSON "+ + "string array of transaction hashes: %v\n", err) + return rcError + } + for _, txHashStr := range txHashStrs { + txHash, err := chainhash.NewHashFromStr(txHashStr) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to parse "+ + "transaction hash %q: %v\n", txHashStr, err) + return rcError + } + txHashes = append(txHashes, txHash) + } + } else { + // Parse the provided transaction hash string. + arg0 = strings.Trim(arg0, `"`) + txHash, err := chainhash.NewHashFromStr(arg0) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to parse transaction "+ + "hash %q: %v\n", arg0, err) + return rcError + } + txHashes = append(txHashes, txHash) + } + + // Connect to dcrd RPC server using websockets. + certs, err := ioutil.ReadFile(cfg.RPCCert) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read RPC server TLS cert: %v\n", + err) + return rcError + } + connCfg := &dcrrpcclient.ConnConfig{ + Host: cfg.RPCServer, + Endpoint: "ws", + User: cfg.RPCUser, + Pass: cfg.RPCPassword, + Certificates: certs, + } + client, err := dcrrpcclient.New(connCfg, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to connect to dcrd RPC server: "+ + "%v\n", err) + return rcError + } + defer client.Shutdown() + + // Check all of the provided transactions. + var hasDevPremineOuts bool + for _, txHash := range txHashes { + // Get a list of all dev premine outpoints the are ancestors of + // all inputs to the provided transaction. + devPremineOuts, err := traceDevPremineOuts(client, txHash) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return rcError + } + + // List outputs which are dev premine outputs. + if len(devPremineOuts) > 0 { + hasDevPremineOuts = true + + // Don't print anything in quiet mode. + if cfg.Quiet { + continue + } + fmt.Printf("Transaction %v contains inputs which "+ + "trace back to the following original dev "+ + "premine outpoints:\n", txHash) + for _, out := range devPremineOuts { + fmt.Println(out) + } + } + } + + // Return the approriate code depending on whether or not any of the + // inputs trace back to a dev premine outpoint. + if hasDevPremineOuts { + return rcDevPremineInputs + } + return rcNoDevPremineInputs +} + +func main() { + os.Exit(realMain()) +}