Accept feexpub once at startup. (#97)
This commit is contained in:
parent
dcfc2e969d
commit
d407af35c0
57
config.go
57
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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
2
main.go
2
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()
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user