Import xpub tweaks (#41)

This commit is contained in:
Jamie Holdstock 2020-05-20 15:18:24 +01:00 committed by GitHub
parent 67c8e8f27c
commit 36c748ba12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 70 additions and 54 deletions

View File

@ -20,6 +20,9 @@
stores it in the database. This key is used to sign all API responses, and the stores it in the database. This key is used to sign all API responses, and the
signature is included in the response header `VSP-Signature`. Error responses signature is included in the response header `VSP-Signature`. Error responses
are not signed. are not signed.
- An xpub key is provided to dcrvsp via config. The first time dcrvsp starts, it
imports this xpub to create a new wallet account. This account is used to
derive addresses for fee payments.
- VSP API as described in [dcrstakepool #574](https://github.com/decred/dcrstakepool/issues/574) - VSP API as described in [dcrstakepool #574](https://github.com/decred/dcrstakepool/issues/574)
- Request fee amount (`GET /fee`) - Request fee amount (`GET /fee`)
- Request fee address (`POST /feeaddress`) - Request fee address (`POST /feeaddress`)

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
@ -31,8 +32,8 @@ type config struct {
Listen string `long:"listen" ini-name:"listen" description:"The ip:port to listen for API requests."` 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"` 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"` 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"` FeeXPub string `long:"feexpub" ini-name:"feexpub" description:"Cold wallet xpub used for collecting fees."`
VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"The fee percentage charged for VSP use. eg. 0.01 (1%), 0.05 (5%)."` VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee percentage charged for VSP use. eg. 0.01 (1%), 0.05 (5%)."`
HomeDir string `long:"homedir" ini-name:"homedir" no-ini:"true" description:"Path to application home directory. Used for storing VSP database and logs."` 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."` ConfigFile string `long:"configfile" ini-name:"configfile" no-ini:"true" description:"Path to configuration file."`
WalletHost string `long:"wallethost" ini-name:"wallethost" description:"The ip:port to establish a JSON-RPC connection with dcrwallet."` WalletHost string `long:"wallethost" ini-name:"wallethost" description:"The ip:port to establish a JSON-RPC connection with dcrwallet."`
@ -181,11 +182,9 @@ func loadConfig() (*config, error) {
} }
// Create the home directory if it doesn't already exist. // Create the home directory if it doesn't already exist.
funcName := "loadConfig"
err = os.MkdirAll(cfg.HomeDir, 0700) err = os.MkdirAll(cfg.HomeDir, 0700)
if err != nil { if err != nil {
str := "%s: failed to create home directory: %v" err := fmt.Errorf("failed to create home directory: %v", err)
err := fmt.Errorf(str, funcName, err)
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
return nil, err return nil, err
} }
@ -233,23 +232,17 @@ func loadConfig() (*config, error) {
// Ensure the dcrwallet RPC username is set. // Ensure the dcrwallet RPC username is set.
if cfg.WalletUser == "" { if cfg.WalletUser == "" {
str := "%s: the walletuser option is not set" return nil, errors.New("the walletuser option is not set")
err := fmt.Errorf(str, funcName)
return nil, err
} }
// Ensure the dcrwallet RPC password is set. // Ensure the dcrwallet RPC password is set.
if cfg.WalletPass == "" { if cfg.WalletPass == "" {
str := "%s: the walletpass option is not set" return nil, errors.New("the walletpass option is not set")
err := fmt.Errorf(str, funcName)
return nil, err
} }
// Ensure the dcrwallet RPC cert path is set. // Ensure the dcrwallet RPC cert path is set.
if cfg.WalletCert == "" { if cfg.WalletCert == "" {
str := "%s: the walletcert option is not set" return nil, errors.New("the walletcert option is not set")
err := fmt.Errorf(str, funcName)
return nil, err
} }
// Add default port for the active network if there is no port specified. // Add default port for the active network if there is no port specified.
@ -259,18 +252,14 @@ func loadConfig() (*config, error) {
cfg.WalletCert = cleanAndExpandPath(cfg.WalletCert) cfg.WalletCert = cleanAndExpandPath(cfg.WalletCert)
cfg.dcrwCert, err = ioutil.ReadFile(cfg.WalletCert) cfg.dcrwCert, err = ioutil.ReadFile(cfg.WalletCert)
if err != nil { if err != nil {
str := "%s: failed to read dcrwallet cert file: %s" return nil, fmt.Errorf("failed to read dcrwallet cert file: %v", err)
err := fmt.Errorf(str, funcName, err)
return nil, err
} }
// Create the data directory. // Create the data directory.
dataDir := filepath.Join(cfg.HomeDir, "data", cfg.netParams.Name) dataDir := filepath.Join(cfg.HomeDir, "data", cfg.netParams.Name)
err = os.MkdirAll(dataDir, 0700) err = os.MkdirAll(dataDir, 0700)
if err != nil { if err != nil {
str := "%s: failed to create data directory: %v" return nil, fmt.Errorf("failed to create data directory: %v", err)
err := fmt.Errorf(str, funcName, err)
return nil, err
} }
// Initialize loggers and log rotation. // Initialize loggers and log rotation.
@ -282,9 +271,12 @@ func loadConfig() (*config, error) {
cfg.dbPath = filepath.Join(dataDir, "vsp.db") cfg.dbPath = filepath.Join(dataDir, "vsp.db")
// Validate the cold wallet xpub. // 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) _, err = hdkeychain.NewKeyFromString(cfg.FeeXPub, cfg.netParams.Params)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to parse feexpub: %v", err)
} }
return &cfg, nil return &cfg, nil

76
main.go
View File

@ -10,6 +10,11 @@ import (
"github.com/jholdstock/dcrvsp/database" "github.com/jholdstock/dcrvsp/database"
"github.com/jholdstock/dcrvsp/rpc" "github.com/jholdstock/dcrvsp/rpc"
"github.com/jholdstock/dcrvsp/webapi" "github.com/jholdstock/dcrvsp/webapi"
"github.com/jrick/wsrpc/v2"
)
const (
feeAccountName = "fees"
) )
func main() { func main() {
@ -68,42 +73,23 @@ func run(ctx context.Context) error {
shutdownWg.Wait() shutdownWg.Wait()
return err return err
} }
// Get the masterpubkey from the fees account, if it exists, and make // Ensure the wallet account for collecting fees exists and matches config.
// sure it matches the configuration. err = setupFeeAccount(ctx, walletClient, cfg.FeeXPub)
var existingXPub string
err = walletClient.Call(ctx, "getmasterpubkey", &existingXPub, "fees")
if err != nil { if err != nil {
// TODO - ignore account not found log.Errorf("Fee account error: %v", err)
log.Errorf("dcrwallet RPC error: %v", err)
requestShutdown() requestShutdown()
shutdownWg.Wait() shutdownWg.Wait()
return err return err
} }
// account exists - make sure it matches the configuration.
if existingXPub != cfg.FeeXPub {
log.Errorf("fees account xpub differs: %s != %s", existingXPub, cfg.FeeXPub)
requestShutdown()
shutdownWg.Wait()
return err
} else {
// account does not exist - import xpub from configuration.
if err = walletClient.Call(ctx, "importxpub", nil, "fees"); err != nil {
log.Errorf("failed to import xpub: %v", err)
requestShutdown()
shutdownWg.Wait()
return err
}
}
// Create and start webapi server. // Create and start webapi server.
apiCfg := webapi.Config{ apiCfg := webapi.Config{
SignKey: signKey, SignKey: signKey,
PubKey: pubKey, PubKey: pubKey,
VSPFee: cfg.VSPFee, VSPFee: cfg.VSPFee,
NetParams: cfg.netParams.Params, NetParams: cfg.netParams.Params,
FeeAccountName: feeAccountName,
} }
// TODO: Make releaseMode properly configurable. Release mode enables very // TODO: Make releaseMode properly configurable. Release mode enables very
// detailed webserver logging and live reloading of HTML templates. // detailed webserver logging and live reloading of HTML templates.
@ -121,3 +107,37 @@ func run(ctx context.Context) error {
return ctx.Err() return ctx.Err()
} }
func setupFeeAccount(ctx context.Context, walletClient *wsrpc.Client, feeXpub string) error {
// Check if account for fee collection already exists.
var accounts map[string]float64
err := walletClient.Call(ctx, "listaccounts", &accounts)
if err != nil {
return fmt.Errorf("dcrwallet RPC error: %v", err)
}
if _, ok := accounts[feeAccountName]; ok {
// Account already exists. Check xpub matches xpub from config.
var existingXPub string
err = walletClient.Call(ctx, "getmasterpubkey", &existingXPub, feeAccountName)
if err != nil {
return fmt.Errorf("dcrwallet RPC error: %v", err)
}
if existingXPub != feeXpub {
return fmt.Errorf("existing account xpub differs from config: %s != %s", existingXPub, feeXpub)
}
log.Debugf("Using existing wallet account %q to collect fees", feeAccountName)
} else {
// Account does not exist. Create it using xpub from config.
if err = walletClient.Call(ctx, "importxpub", nil, feeAccountName, feeXpub); err != nil {
log.Errorf("Failed to import xpub: %v", err)
return err
}
log.Debugf("Created new wallet account %q to collect fees", feeAccountName)
}
return nil
}

View File

@ -205,7 +205,7 @@ func feeAddress(c *gin.Context) {
} }
var newAddress string var newAddress string
err = walletClient.Call(ctx, "getnewaddress", &newAddress, "fees") err = walletClient.Call(ctx, "getnewaddress", &newAddress, cfg.FeeAccountName)
if err != nil { if err != nil {
log.Errorf("GetNewAddress error: %v", err) log.Errorf("GetNewAddress error: %v", err)
sendErrorResponse("unable to generate fee address", http.StatusInternalServerError, c) sendErrorResponse("unable to generate fee address", http.StatusInternalServerError, c)

View File

@ -16,10 +16,11 @@ import (
) )
type Config struct { type Config struct {
SignKey ed25519.PrivateKey SignKey ed25519.PrivateKey
PubKey ed25519.PublicKey PubKey ed25519.PublicKey
VSPFee float64 VSPFee float64
NetParams *chaincfg.Params NetParams *chaincfg.Params
FeeAccountName string
} }
var cfg Config var cfg Config