From d407af35c099efadeed8c7677d9d4eaea521411b Mon Sep 17 00:00:00 2001 From: Jamie Holdstock Date: Wed, 3 Jun 2020 18:10:30 +0100 Subject: [PATCH] Accept feexpub once at startup. (#97) --- config.go | 57 ++++++++++++----- database/database.go | 125 +++++++++++++++++++++++++------------- database/database_test.go | 6 +- docs/deployment.md | 40 ++++++++---- main.go | 2 +- webapi/webapi.go | 10 ++- 6 files changed, 166 insertions(+), 74 deletions(-) diff --git a/config.go b/config.go index 8d21898..cda4531 100644 --- a/config.go +++ b/config.go @@ -15,6 +15,7 @@ import ( "decred.org/dcrwallet/wallet/txrules" "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrd/hdkeychain/v3" + "github.com/decred/vspd/database" flags "github.com/jessevdk/go-flags" ) @@ -38,10 +39,7 @@ type config struct { Listen string `long:"listen" ini-name:"listen" description:"The ip:port to listen for API requests."` LogLevel string `long:"loglevel" ini-name:"loglevel" description:"Logging level." choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"critical"` Network string `long:"network" ini-name:"network" description:"Decred network to use." choice:"testnet" choice:"mainnet" choice:"simnet"` - FeeXPub string `long:"feexpub" ini-name:"feexpub" description:"Cold wallet xpub used for collecting fees."` VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee percentage charged for VSP use. eg. 2.0 (2%), 0.5 (0.5%)."` - HomeDir string `long:"homedir" ini-name:"homedir" no-ini:"true" description:"Path to application home directory. Used for storing VSP database and logs."` - ConfigFile string `long:"configfile" ini-name:"configfile" no-ini:"true" description:"Path to configuration file."` DcrdHost string `long:"dcrdhost" ini-name:"dcrdhost" description:"The ip:port to establish a JSON-RPC connection with dcrd. Should be the same host where vspd is running."` DcrdUser string `long:"dcrduser" ini-name:"dcrduser" description:"Username for dcrd RPC connections."` DcrdPass string `long:"dcrdpass" ini-name:"dcrdpass" description:"Password for dcrd RPC connections."` @@ -55,6 +53,11 @@ type config struct { BackupInterval time.Duration `long:"backupinterval" ini-name:"backupinterval" description:"Time period between automatic database backups. Valid time units are {s,m,h}. Minimum 30 seconds."` VspClosed bool `long:"vspclosed" ini-name:"vspclosed" description:"Closed prevents the VSP from accepting new tickets."` + // The following flags should be set on CLI only, not via config file. + FeeXPub string `long:"feexpub" no-ini:"true" description:"Cold wallet xpub used for collecting fees. Should be provided once to initialize a vspd database."` + HomeDir string `long:"homedir" no-ini:"true" description:"Path to application home directory. Used for storing VSP database and logs."` + ConfigFile string `long:"configfile" no-ini:"true" description:"Path to configuration file."` + dbPath string netParams *netParams dcrdCert []byte @@ -172,8 +175,7 @@ func loadConfig() (*config, error) { _, err := preParser.Parse() if err != nil { if e, ok := err.(*flags.Error); ok && e.Type != flags.ErrHelp { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) + return nil, err } else if ok && e.Type == flags.ErrHelp { fmt.Fprintln(os.Stdout, err) os.Exit(0) @@ -211,7 +213,6 @@ func loadConfig() (*config, error) { // Create a default config file when one does not exist and the user did // not specify an override. if preCfg.ConfigFile == defaultConfigFile && !fileExists(preCfg.ConfigFile) { - fmt.Printf("Writing a config file with default values to %s\n", defaultConfigFile) preIni := flags.NewIniParser(preParser) err = preIni.WriteFile(preCfg.ConfigFile, flags.IniIncludeComments|flags.IniIncludeDefaults) @@ -219,6 +220,11 @@ func loadConfig() (*config, error) { return nil, fmt.Errorf("error creating a default "+ "config file: %v", err) } + fmt.Printf("Config file with default values written to %s\n", defaultConfigFile) + + // File created, user now has to fill in values. Proceeding with the + // default file just causes errors. + os.Exit(0) } // Load additional config from file. @@ -226,8 +232,7 @@ func loadConfig() (*config, error) { err = flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile) if err != nil { - fmt.Fprintf(os.Stderr, "error parsing config file: %v\n", err) - os.Exit(1) + return nil, fmt.Errorf("error parsing config file: %v", err) } // Parse command line options again to ensure they take precedence. @@ -337,13 +342,35 @@ func loadConfig() (*config, error) { // Set the database path cfg.dbPath = filepath.Join(dataDir, "vspd.db") - // Validate the cold wallet xpub. - if cfg.FeeXPub == "" { - return nil, errors.New("the feexpub option is not set") - } - _, err = hdkeychain.NewKeyFromString(cfg.FeeXPub, cfg.netParams.Params) - if err != nil { - return nil, fmt.Errorf("failed to parse feexpub: %v", err) + // If xpub has been provided, create a new database and exit. + if cfg.FeeXPub != "" { + // If database already exists, return error. + if fileExists(cfg.dbPath) { + return nil, fmt.Errorf("database already initialized at %s, "+ + "--feexpub option is not needed.", cfg.dbPath) + } + + // Ensure provided value is a valid key for the selected network. + _, err = hdkeychain.NewKeyFromString(cfg.FeeXPub, cfg.netParams.Params) + if err != nil { + return nil, fmt.Errorf("failed to parse feexpub: %v", err) + } + + // Create new database. + err = database.CreateNew(cfg.dbPath, cfg.FeeXPub) + if err != nil { + return nil, fmt.Errorf("error creating db file %s: %v", cfg.dbPath, err) + } + + // Exit with success + os.Exit(0) + + } else { + // If database does not exist, return error. + if !fileExists(cfg.dbPath) { + return nil, fmt.Errorf("no database exists in %s. Run vspd with the"+ + " --feexpub option to initialize one.", dataDir) + } } return &cfg, nil diff --git a/database/database.go b/database/database.go index 194a644..e36c1d2 100644 --- a/database/database.go +++ b/database/database.go @@ -28,6 +28,8 @@ var ( ticketBktK = []byte("ticketbkt") // version is the current database version. versionK = []byte("version") + // feeXPub is the extended public key used for collecting VSP fees. + feeXPubK = []byte("feeXPub") // privatekey is the private key. privateKeyK = []byte("privatekey") // lastaddressindex is the index of the last address used for fees. @@ -63,6 +65,69 @@ func writeBackup(db *bolt.DB, dbFile string) error { return err } +func CreateNew(dbFile, feeXPub string) error { + log.Infof("Initializing new database at %s", dbFile) + + db, err := bolt.Open(dbFile, 0600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return fmt.Errorf("unable to open db file: %v", err) + } + + defer db.Close() + + // Create all storage buckets of the VSP if they don't already exist. + err = db.Update(func(tx *bolt.Tx) error { + // Create parent bucket. + vspBkt, err := tx.CreateBucket(vspBktK) + if err != nil { + return fmt.Errorf("failed to create %s bucket: %v", string(vspBktK), err) + } + + // Initialize with database version 1. + vbytes := make([]byte, 4) + binary.LittleEndian.PutUint32(vbytes, uint32(1)) + err = vspBkt.Put(versionK, vbytes) + if err != nil { + return err + } + + log.Info("Generating ed25519 signing key") + + // Generate ed25519 key + _, signKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return fmt.Errorf("failed to generate signing key: %v", err) + } + err = vspBkt.Put(privateKeyK, signKey.Seed()) + if err != nil { + return err + } + + log.Info("Storing extended public key") + // Store fee xpub + err = vspBkt.Put(feeXPubK, []byte(feeXPub)) + if err != nil { + return err + } + + // Create ticket bucket. + _, err = vspBkt.CreateBucket(ticketBktK) + if err != nil { + return fmt.Errorf("failed to create %s bucket: %v", string(ticketBktK), err) + } + + return nil + }) + + if err != nil { + return err + } + + log.Info("Database initialized") + + return nil +} + // Open initializes and returns an open database. If no database file is found // at the provided path, a new one will be created. func Open(ctx context.Context, shutdownWg *sync.WaitGroup, dbFile string, backupInterval time.Duration) (*VspDatabase, error) { @@ -115,48 +180,6 @@ func Open(ctx context.Context, shutdownWg *sync.WaitGroup, dbFile string, backup } }() - // Create all storage buckets of the VSP if they don't already exist. - err = db.Update(func(tx *bolt.Tx) error { - if tx.Bucket(vspBktK) == nil { - log.Debug("Initializing new database") - // Create parent bucket. - vspBkt, err := tx.CreateBucket(vspBktK) - if err != nil { - return fmt.Errorf("failed to create %s bucket: %v", string(vspBktK), err) - } - - // Initialize with database version 1. - vbytes := make([]byte, 4) - binary.LittleEndian.PutUint32(vbytes, uint32(1)) - err = vspBkt.Put(versionK, vbytes) - if err != nil { - return err - } - - // Generate ed25519 key - _, signKey, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return fmt.Errorf("failed to generate signing key: %v", err) - } - err = vspBkt.Put(privateKeyK, signKey.Seed()) - if err != nil { - return err - } - - // Create ticket bucket. - _, err = vspBkt.CreateBucket(ticketBktK) - if err != nil { - return fmt.Errorf("failed to create %s bucket: %v", string(ticketBktK), err) - } - } - - return nil - }) - - if err != nil { - return nil, err - } - return &VspDatabase{db: db}, nil } @@ -187,3 +210,21 @@ func (vdb *VspDatabase) KeyPair() (ed25519.PrivateKey, ed25519.PublicKey, error) return signKey, pubKey, err } + +func (vdb *VspDatabase) GetFeeXPub() (string, error) { + var feeXPub string + err := vdb.db.View(func(tx *bolt.Tx) error { + vspBkt := tx.Bucket(vspBktK) + + xpubBytes := vspBkt.Get(feeXPubK) + if xpubBytes == nil { + return nil + } + + feeXPub = string(xpubBytes) + + return nil + }) + + return feeXPub, err +} diff --git a/database/database_test.go b/database/database_test.go index 7e1e1cf..eeabf23 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -34,10 +34,14 @@ func TestDatabase(t *testing.T) { var err error var wg sync.WaitGroup ctx, cancel := context.WithCancel(context.TODO()) - db, err = Open(ctx, &wg, testDb, time.Hour) + err = CreateNew(testDb, "feexpub") if err != nil { t.Fatalf("error creating test database: %v", err) } + db, err = Open(ctx, &wg, testDb, time.Hour) + if err != nil { + t.Fatalf("error opening test database: %v", err) + } // Run the sub-test. t.Run(testName, test) diff --git a/docs/deployment.md b/docs/deployment.md index 98e624f..819e8c6 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -20,17 +20,6 @@ should be used to export an extended public (xpub) key from one of the wallet accounts. This xpub key will be provided to vspd via config, and vspd will use it to derive a new addresses for receiving fee payments. -## Front-end Server - -The front-end server is where vspd will be running. The port vspd is listening -on (default `3000`) should be available for clients to reach over the internet. -This port is used for both the API and serving the HTML front end. - -dcrd needs to be running on this server with transaction index enabled -(`--txindex`). dcrd is used for fishing ticket details out of the chain, for -receiving `blockconnected` notifications, and for broadcasting and checking the -status of fee transactions. - ## Voting Servers A vspd deployment should have a minimum of three remote voting wallets. The @@ -43,6 +32,31 @@ purpose. dcrwallet should be permenantly unlocked and have voting enabled (`--enablevoting`). vspd on the front-end server must be able to reach each instance of dcrwallet over RPC. +## Front-end Server + +The front-end server is where vspd will be running. The port vspd is listening +on (default `3000`) should be available for clients to reach over the internet. +This port is used for both the API, and for serving the HTML front end. + +1. Start an instance of dcrd on this server with transaction index enabled + (`--txindex`). dcrd is used for fishing ticket details out of the chain, for + receiving `blockconnected` notifications, and for broadcasting and checking + the status of fee transactions. + +1. Run `vspd` with no arguments to write a default config file. Modify the + config file to set your dcrd and dcrwallet connection details, and any other + required customization. + +1. A vspd database must be initialized before vpsd can be started. To do this, + provide vspd with the xpub key it should use for collecting fees: + + ```no-highlight + $ vspd --feexpub=tpubVppjaMjp8GEW... + ``` + +1. Once the database is initialized, vspd can be started for normal operation by + running it without the `--feexpub` flag. + ## Deploying alongside dcrstakepool It is possible to run vspd on the same infrastructure as an existing @@ -75,4 +89,6 @@ database file will also be written to this path when vspd shuts down. ## Disaster Recovery -// TODO +The database file contains everything needed to restore a vspd deployment - +simply place the database file into the vspd data directory and start vspd as +normal. diff --git a/main.go b/main.go index 81e2585..1e56d07 100644 --- a/main.go +++ b/main.go @@ -111,7 +111,7 @@ func run(ctx context.Context) error { VspClosed: cfg.VspClosed, } err = webapi.Start(ctx, shutdownRequestChannel, &shutdownWg, cfg.Listen, db, - dcrd, wallets, cfg.WebServerDebug, cfg.FeeXPub, apiCfg) + dcrd, wallets, cfg.WebServerDebug, apiCfg) if err != nil { log.Errorf("Failed to initialize webapi: %v", err) requestShutdown() diff --git a/webapi/webapi.go b/webapi/webapi.go index fb112db..6a843a1 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -41,7 +41,7 @@ var signPrivKey ed25519.PrivateKey var signPubKey ed25519.PublicKey func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *sync.WaitGroup, - listen string, vdb *database.VspDatabase, dConnect rpc.DcrdConnect, wConnect rpc.WalletConnect, debugMode bool, feeXPub string, config Config) error { + listen string, vdb *database.VspDatabase, dConnect rpc.DcrdConnect, wConnect rpc.WalletConnect, debugMode bool, config Config) error { cfg = config db = vdb @@ -62,12 +62,16 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s return fmt.Errorf("could not initialize homepage data: %v", err) } - // Get the last used address index from the database, and use it to - // initialize the address generator. + // Get the last used address index and the feeXpub from the database, and + // use them to initialize the address generator. idx, err := vdb.GetLastAddressIndex() if err != nil { return fmt.Errorf("GetLastAddressIndex error: %v", err) } + feeXPub, err := vdb.GetFeeXPub() + if err != nil { + return fmt.Errorf("GetFeeXPub error: %v", err) + } addrGen, err = newAddressGenerator(feeXPub, config.NetParams, idx) if err != nil { return fmt.Errorf("failed to initialize fee address generator: %v", err)