From d00d9c83b7d793cf03e1204c7765a0b2574d944e Mon Sep 17 00:00:00 2001 From: jholdstock Date: Thu, 23 May 2024 09:55:37 +0100 Subject: [PATCH 1/3] vspd: Accessors for derived config values. This enables the derived config values, which are not exported, to still be accessed after config.go is moved to the internal vspd package. --- cmd/vspd/config.go | 48 ++++++++++++++++++++++++++++++---------------- cmd/vspd/main.go | 19 +++++++++++------- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/cmd/vspd/config.go b/cmd/vspd/config.go index 3946b034..ecc8324e 100644 --- a/cmd/vspd/config.go +++ b/cmd/vspd/config.go @@ -59,14 +59,34 @@ type vspdConfig struct { 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:"DEPRECATED: This behavior is no longer available and this option will be removed in a future version of the software."` - logPath string - dbPath string + // The following fields are derived from the above fields by loadConfig(). + dataDir string network *config.Network dcrdCert []byte walletHosts, walletUsers, walletPasswords []string walletCerts [][]byte } +func (cfg *vspdConfig) Network() *config.Network { + return cfg.network +} + +func (cfg *vspdConfig) LogDir() string { + return filepath.Join(cfg.HomeDir, "logs", cfg.network.Name) +} + +func (cfg *vspdConfig) DatabaseFile() string { + return filepath.Join(cfg.dataDir, dbFilename) +} + +func (cfg *vspdConfig) DcrdDetails() (string, string, string, []byte) { + return cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert +} + +func (cfg *vspdConfig) WalletDetails() ([]string, []string, []string, [][]byte) { + return cfg.walletUsers, cfg.walletPasswords, cfg.walletHosts, cfg.walletCerts +} + var defaultConfig = vspdConfig{ Listen: ":8800", LogLevel: "debug", @@ -370,24 +390,20 @@ func loadConfig() (*vspdConfig, error) { cfg.DcrdHost = normalizeAddress(cfg.DcrdHost, cfg.network.DcrdRPCServerPort) // Create the data directory. - dataDir := filepath.Join(cfg.HomeDir, "data", cfg.network.Name) - err = os.MkdirAll(dataDir, 0700) + cfg.dataDir = filepath.Join(cfg.HomeDir, "data", cfg.network.Name) + err = os.MkdirAll(cfg.dataDir, 0700) if err != nil { return nil, fmt.Errorf("failed to create data directory: %w", err) } - // Set the log path. - cfg.logPath = filepath.Join(cfg.HomeDir, "logs", cfg.network.Name) - - // Set the database path. - cfg.dbPath = filepath.Join(dataDir, dbFilename) + dbPath := cfg.DatabaseFile() // If xpub has been provided, create a new database and exit. if cfg.FeeXPub != "" { // If database already exists, return error. - if fileExists(cfg.dbPath) { + if fileExists(dbPath) { return nil, fmt.Errorf("database already initialized at %s, "+ - "--feexpub option is not needed", cfg.dbPath) + "--feexpub option is not needed", dbPath) } // Ensure provided value is a valid key for the selected network. @@ -397,10 +413,10 @@ func loadConfig() (*vspdConfig, error) { } // Create new database. - fmt.Printf("Initializing new database at %s\n", cfg.dbPath) - err = database.CreateNew(cfg.dbPath, cfg.FeeXPub) + fmt.Printf("Initializing new database at %s\n", dbPath) + err = database.CreateNew(dbPath, cfg.FeeXPub) if err != nil { - return nil, fmt.Errorf("error creating db file %s: %w", cfg.dbPath, err) + return nil, fmt.Errorf("error creating db file %s: %w", dbPath, err) } // Exit with success @@ -409,9 +425,9 @@ func loadConfig() (*vspdConfig, error) { } // If database does not exist, return error. - if !fileExists(cfg.dbPath) { + if !fileExists(dbPath) { return nil, fmt.Errorf("no database exists in %s. Run vspd with the"+ - " --feexpub option to initialize one", dataDir) + " --feexpub option to initialize one", cfg.dataDir) } return &cfg, nil diff --git a/cmd/vspd/main.go b/cmd/vspd/main.go index f3e4dbac..0bf2c45e 100644 --- a/cmd/vspd/main.go +++ b/cmd/vspd/main.go @@ -38,7 +38,7 @@ func main() { // returns a function which can be used to create ready-to-use subsystem // loggers. func initLogging(cfg *vspdConfig) (func(subsystem string) slog.Logger, error) { - backend, err := newLogBackend(cfg.logPath, "vspd", cfg.MaxLogSize, cfg.LogsToKeep) + backend, err := newLogBackend(cfg.LogDir(), "vspd", cfg.MaxLogSize, cfg.LogsToKeep) if err != nil { return nil, fmt.Errorf("failed to initialize logger: %w", err) } @@ -82,7 +82,9 @@ func run() int { log.Criticalf("Version %s (Go version %s %s/%s)", version.String(), runtime.Version(), runtime.GOOS, runtime.GOARCH) - if cfg.network == &config.MainNet && version.IsPreRelease() { + network := cfg.Network() + + if network == &config.MainNet && version.IsPreRelease() { log.Warnf("") log.Warnf("\tWARNING: This is a pre-release version of vspd which should not be used on mainnet") log.Warnf("") @@ -101,7 +103,7 @@ func run() int { } // Open database. - db, err := database.Open(cfg.dbPath, makeLogger(" DB"), maxVoteChangeRecords) + db, err := database.Open(cfg.DatabaseFile(), makeLogger(" DB"), maxVoteChangeRecords) if err != nil { log.Errorf("Failed to open database: %v", err) return 1 @@ -116,18 +118,21 @@ func run() int { // Create RPC client for local dcrd instance (used for broadcasting and // checking the status of fee transactions). - dcrd := rpc.SetupDcrd(cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert, cfg.network.Params, rpcLog, blockNotifChan) + dUser, dPass, dHost, dCert := cfg.DcrdDetails() + dcrd := rpc.SetupDcrd(dUser, dPass, dHost, dCert, network.Params, rpcLog, blockNotifChan) + defer dcrd.Close() // Create RPC client for remote dcrwallet instances (used for voting). - wallets := rpc.SetupWallet(cfg.walletUsers, cfg.walletPasswords, cfg.walletHosts, cfg.walletCerts, cfg.network.Params, rpcLog) + wUsers, wPasswords, wHosts, wCerts := cfg.WalletDetails() + wallets := rpc.SetupWallet(wUsers, wPasswords, wHosts, wCerts, network.Params, rpcLog) defer wallets.Close() // Create webapi server. apiCfg := webapi.Config{ Listen: cfg.Listen, VSPFee: cfg.VSPFee, - Network: cfg.network, + Network: network, SupportEmail: cfg.SupportEmail, VspClosed: cfg.VspClosed, VspClosedMsg: cfg.VspClosedMsg, @@ -154,7 +159,7 @@ func run() int { }() // Start vspd. - vspd := vspd.New(cfg.network, log, db, dcrd, wallets, blockNotifChan) + vspd := vspd.New(network, log, db, dcrd, wallets, blockNotifChan) wg.Add(1) go func() { vspd.Run(ctx) From 2c96adbea90bde0b9009ecb72ecb22eb7b7abca8 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Thu, 23 May 2024 09:59:41 +0100 Subject: [PATCH 2/3] vspd: Move config to internal package. This enables the config to be reused in multiple binaries - eg. the upcoming vsp admin binary which will be responsible for writing a default config file for new vspd deployments. --- cmd/vspd/main.go | 4 ++-- {cmd => internal}/vspd/config.go | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) rename {cmd => internal}/vspd/config.go (96%) diff --git a/cmd/vspd/main.go b/cmd/vspd/main.go index 0bf2c45e..904a0c4b 100644 --- a/cmd/vspd/main.go +++ b/cmd/vspd/main.go @@ -37,7 +37,7 @@ func main() { // initLogging uses the provided vspd config to create a logging backend, and // returns a function which can be used to create ready-to-use subsystem // loggers. -func initLogging(cfg *vspdConfig) (func(subsystem string) slog.Logger, error) { +func initLogging(cfg *vspd.Config) (func(subsystem string) slog.Logger, error) { backend, err := newLogBackend(cfg.LogDir(), "vspd", cfg.MaxLogSize, cfg.LogsToKeep) if err != nil { return nil, fmt.Errorf("failed to initialize logger: %w", err) @@ -60,7 +60,7 @@ func initLogging(cfg *vspdConfig) (func(subsystem string) slog.Logger, error) { // fact that deferred functions do not run when os.Exit() is called. func run() int { // Load config file and parse CLI args. - cfg, err := loadConfig() + cfg, err := vspd.LoadConfig() if err != nil { fmt.Fprintf(os.Stderr, "loadConfig error: %v\n", err) return 1 diff --git a/cmd/vspd/config.go b/internal/vspd/config.go similarity index 96% rename from cmd/vspd/config.go rename to internal/vspd/config.go index ecc8324e..95b32e46 100644 --- a/cmd/vspd/config.go +++ b/internal/vspd/config.go @@ -2,7 +2,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package main +package vspd import ( "errors" @@ -29,8 +29,8 @@ const ( dbFilename = "vspd.db" ) -// vspdConfig defines the configuration options for the vspd process. -type vspdConfig struct { +// Config defines the configuration options for the vspd process. +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"` MaxLogSize int64 `long:"maxlogsize" ini-name:"maxlogsize" description:"File size threshold for log file rotation (MB)."` @@ -59,7 +59,7 @@ type vspdConfig struct { 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:"DEPRECATED: This behavior is no longer available and this option will be removed in a future version of the software."` - // The following fields are derived from the above fields by loadConfig(). + // The following fields are derived from the above fields by LoadConfig(). dataDir string network *config.Network dcrdCert []byte @@ -67,27 +67,27 @@ type vspdConfig struct { walletCerts [][]byte } -func (cfg *vspdConfig) Network() *config.Network { +func (cfg *Config) Network() *config.Network { return cfg.network } -func (cfg *vspdConfig) LogDir() string { +func (cfg *Config) LogDir() string { return filepath.Join(cfg.HomeDir, "logs", cfg.network.Name) } -func (cfg *vspdConfig) DatabaseFile() string { +func (cfg *Config) DatabaseFile() string { return filepath.Join(cfg.dataDir, dbFilename) } -func (cfg *vspdConfig) DcrdDetails() (string, string, string, []byte) { +func (cfg *Config) DcrdDetails() (string, string, string, []byte) { return cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert } -func (cfg *vspdConfig) WalletDetails() ([]string, []string, []string, [][]byte) { +func (cfg *Config) WalletDetails() ([]string, []string, []string, [][]byte) { return cfg.walletUsers, cfg.walletPasswords, cfg.walletHosts, cfg.walletCerts } -var defaultConfig = vspdConfig{ +var DefaultConfig = Config{ Listen: ":8800", LogLevel: "debug", MaxLogSize: int64(10), @@ -175,7 +175,7 @@ func normalizeAddress(addr, defaultPort string) string { return addr } -// loadConfig initializes and parses the config using a config file and command +// LoadConfig initializes and parses the config using a config file and command // line options. // // The configuration proceeds as follows: @@ -187,8 +187,8 @@ func normalizeAddress(addr, defaultPort string) string { // The above results in vspd 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() (*vspdConfig, error) { - cfg := defaultConfig +func LoadConfig() (*Config, error) { + cfg := DefaultConfig // If command line options are requesting help, write it to stdout and exit. if config.WriteHelp(&cfg) { From f4e440dfa7c6ef7fa1057a7e768e5f2fb6d5078b Mon Sep 17 00:00:00 2001 From: jholdstock Date: Wed, 29 May 2024 12:47:22 +0100 Subject: [PATCH 3/3] vspd: Wrap RPC connection details in a struct. Returning a single struct which contains multiple named fields reduces the chance of a mistake in the calling code, as compared to returning multiple unnamed values which are all of the same type. --- cmd/vspd/main.go | 8 ++--- internal/vspd/config.go | 71 +++++++++++++++++++++++++++++------------ 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/cmd/vspd/main.go b/cmd/vspd/main.go index 904a0c4b..c1f52bc6 100644 --- a/cmd/vspd/main.go +++ b/cmd/vspd/main.go @@ -118,14 +118,14 @@ func run() int { // Create RPC client for local dcrd instance (used for broadcasting and // checking the status of fee transactions). - dUser, dPass, dHost, dCert := cfg.DcrdDetails() - dcrd := rpc.SetupDcrd(dUser, dPass, dHost, dCert, network.Params, rpcLog, blockNotifChan) + dd := cfg.DcrdDetails() + dcrd := rpc.SetupDcrd(dd.User, dd.Password, dd.Host, dd.Cert, network.Params, rpcLog, blockNotifChan) defer dcrd.Close() // Create RPC client for remote dcrwallet instances (used for voting). - wUsers, wPasswords, wHosts, wCerts := cfg.WalletDetails() - wallets := rpc.SetupWallet(wUsers, wPasswords, wHosts, wCerts, network.Params, rpcLog) + wd := cfg.WalletDetails() + wallets := rpc.SetupWallet(wd.Users, wd.Passwords, wd.Hosts, wd.Certs, network.Params, rpcLog) defer wallets.Close() // Create webapi server. diff --git a/internal/vspd/config.go b/internal/vspd/config.go index 95b32e46..ae95be06 100644 --- a/internal/vspd/config.go +++ b/internal/vspd/config.go @@ -60,11 +60,24 @@ type Config struct { ConfigFile string `long:"configfile" no-ini:"true" description:"DEPRECATED: This behavior is no longer available and this option will be removed in a future version of the software."` // The following fields are derived from the above fields by LoadConfig(). - dataDir string - network *config.Network - dcrdCert []byte - walletHosts, walletUsers, walletPasswords []string - walletCerts [][]byte + dataDir string + network *config.Network + dcrdDetails *DcrdDetails + walletDetails *WalletDetails +} + +type DcrdDetails struct { + User string + Password string + Host string + Cert []byte +} + +type WalletDetails struct { + Users []string + Passwords []string + Hosts []string + Certs [][]byte } func (cfg *Config) Network() *config.Network { @@ -79,12 +92,12 @@ func (cfg *Config) DatabaseFile() string { return filepath.Join(cfg.dataDir, dbFilename) } -func (cfg *Config) DcrdDetails() (string, string, string, []byte) { - return cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert +func (cfg *Config) DcrdDetails() *DcrdDetails { + return cfg.dcrdDetails } -func (cfg *Config) WalletDetails() ([]string, []string, []string, [][]byte) { - return cfg.walletUsers, cfg.walletPasswords, cfg.walletHosts, cfg.walletCerts +func (cfg *Config) WalletDetails() *WalletDetails { + return cfg.walletDetails } var DefaultConfig = Config{ @@ -319,11 +332,22 @@ func LoadConfig() (*Config, error) { // Load dcrd RPC certificate. cfg.DcrdCert = cleanAndExpandPath(cfg.DcrdCert) - cfg.dcrdCert, err = os.ReadFile(cfg.DcrdCert) + dcrdCert, err := os.ReadFile(cfg.DcrdCert) if err != nil { return nil, fmt.Errorf("failed to read dcrd cert file: %w", err) } + // Add default port for the active network if there is no port specified. + cfg.DcrdHost = normalizeAddress(cfg.DcrdHost, cfg.network.DcrdRPCServerPort) + + // All dcrd connection details are validated and preprocessed. + cfg.dcrdDetails = &DcrdDetails{ + User: cfg.DcrdUser, + Password: cfg.DcrdPass, + Host: cfg.DcrdHost, + Cert: dcrdCert, + } + // Ensure the dcrwallet RPC username is set. if cfg.WalletUsers == "" { return nil, errors.New("the walletuser option is not set") @@ -340,20 +364,20 @@ func LoadConfig() (*Config, error) { } // Parse list of wallet hosts. - cfg.walletHosts = strings.Split(cfg.WalletHosts, ",") - numHost := len(cfg.walletHosts) + walletHosts := strings.Split(cfg.WalletHosts, ",") + numHost := len(walletHosts) // An RPC username must be specified for each wallet host. - cfg.walletUsers = strings.Split(cfg.WalletUsers, ",") - numUser := len(cfg.walletUsers) + walletUsers := strings.Split(cfg.WalletUsers, ",") + numUser := len(walletUsers) if numUser != numHost { return nil, fmt.Errorf("%d wallet hosts specified, expected %d RPC usernames, got %d", numHost, numHost, numUser) } // An RPC password must be specified for each wallet host. - cfg.walletPasswords = strings.Split(cfg.WalletPasswords, ",") - numPass := len(cfg.walletPasswords) + walletPasswords := strings.Split(cfg.WalletPasswords, ",") + numPass := len(walletPasswords) if numPass != numHost { return nil, fmt.Errorf("%d wallet hosts specified, expected %d RPC passwords, got %d", numHost, numHost, numPass) @@ -368,10 +392,10 @@ func LoadConfig() (*Config, error) { } // Load dcrwallet RPC certificate(s). - cfg.walletCerts = make([][]byte, numCert) + walletCerts := make([][]byte, numCert) for i := 0; i < numCert; i++ { certs[i] = cleanAndExpandPath(certs[i]) - cfg.walletCerts[i], err = os.ReadFile(certs[i]) + walletCerts[i], err = os.ReadFile(certs[i]) if err != nil { return nil, fmt.Errorf("failed to read dcrwallet cert file: %w", err) } @@ -385,9 +409,16 @@ func LoadConfig() (*Config, error) { // Add default port for the active network if there is no port specified. for i := 0; i < numHost; i++ { - cfg.walletHosts[i] = normalizeAddress(cfg.walletHosts[i], cfg.network.WalletRPCServerPort) + walletHosts[i] = normalizeAddress(walletHosts[i], cfg.network.WalletRPCServerPort) + } + + // All dcrwallet connection details are validated and preprocessed. + cfg.walletDetails = &WalletDetails{ + Users: walletUsers, + Passwords: walletPasswords, + Hosts: walletHosts, + Certs: walletCerts, } - cfg.DcrdHost = normalizeAddress(cfg.DcrdHost, cfg.network.DcrdRPCServerPort) // Create the data directory. cfg.dataDir = filepath.Join(cfg.HomeDir, "data", cfg.network.Name)