Accept feexpub once at startup. (#97)

This commit is contained in:
Jamie Holdstock 2020-06-03 18:10:30 +01:00 committed by GitHub
parent dcfc2e969d
commit d407af35c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 166 additions and 74 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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.

View File

@ -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()

View File

@ -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)