From 2cdec5a46eb1433faba09b02e7c9e6b6fb49ec6c Mon Sep 17 00:00:00 2001 From: Linden <70739041+linden@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:39:19 -0700 Subject: [PATCH 1/9] feat: add `generatetoaddress` support removes the need to run dedicated wallet when testing on simnet/regtest. --- mining/cpuminer/cpuminer.go | 6 ++++-- rpcserver.go | 37 ++++++++++++++++++++++++++++++------- rpcserverhelp.go | 9 +++++++++ 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/mining/cpuminer/cpuminer.go b/mining/cpuminer/cpuminer.go index 2c07f2ee1f..bd3a5feeb9 100644 --- a/mining/cpuminer/cpuminer.go +++ b/mining/cpuminer/cpuminer.go @@ -104,6 +104,7 @@ type CPUMiner struct { updateHashes chan uint64 speedMonitorQuit chan struct{} quit chan struct{} + MiningAddrs []btcutil.Address } // speedMonitor handles tracking the number of hashes per second the mining @@ -336,7 +337,7 @@ out: // Choose a payment address at random. rand.Seed(time.Now().UnixNano()) - payToAddr := m.cfg.MiningAddrs[rand.Intn(len(m.cfg.MiningAddrs))] + payToAddr := m.MiningAddrs[rand.Intn(len(m.MiningAddrs))] // Create a new block template using the available transactions // in the memory pool as a source of transactions to potentially @@ -590,7 +591,7 @@ func (m *CPUMiner) GenerateNBlocks(n uint32) ([]*chainhash.Hash, error) { // Choose a payment address at random. rand.Seed(time.Now().UnixNano()) - payToAddr := m.cfg.MiningAddrs[rand.Intn(len(m.cfg.MiningAddrs))] + payToAddr := m.MiningAddrs[rand.Intn(len(m.MiningAddrs))] // Create a new block template using the available transactions // in the memory pool as a source of transactions to potentially @@ -638,5 +639,6 @@ func New(cfg *Config) *CPUMiner { updateNumWorkers: make(chan struct{}), queryHashesPerSec: make(chan float64), updateHashes: make(chan uint64), + MiningAddrs: cfg.MiningAddrs, } } diff --git a/rpcserver.go b/rpcserver.go index 363661d787..8c94ba21d5 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -141,6 +141,7 @@ var rpcHandlersBeforeInit = map[string]commandHandler{ "decodescript": handleDecodeScript, "estimatefee": handleEstimateFee, "generate": handleGenerate, + "generatetoaddress": handleGenerateToAddress, "getaddednodeinfo": handleGetAddedNodeInfo, "getbestblock": handleGetBestBlock, "getbestblockhash": handleGetBestBlockHash, @@ -888,8 +889,7 @@ func handleEstimateFee(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) return float64(feeRate), nil } -// handleGenerate handles generate commands. -func handleGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { +func generate(s *rpcServer, blocks uint32) ([]string, error) { // Respond with an error if there are no addresses to pay the // created blocks to. if len(cfg.miningAddrs) == 0 { @@ -912,10 +912,8 @@ func handleGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (i } } - c := cmd.(*btcjson.GenerateCmd) - // Respond with an error if the client is requesting 0 blocks to be generated. - if c.NumBlocks == 0 { + if blocks == 0 { return nil, &btcjson.RPCError{ Code: btcjson.ErrRPCInternal.Code, Message: "Please request a nonzero number of blocks to generate.", @@ -923,9 +921,9 @@ func handleGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (i } // Create a reply - reply := make([]string, c.NumBlocks) + reply := make([]string, blocks) - blockHashes, err := s.cfg.CPUMiner.GenerateNBlocks(c.NumBlocks) + blockHashes, err := s.cfg.CPUMiner.GenerateNBlocks(blocks) if err != nil { return nil, &btcjson.RPCError{ Code: btcjson.ErrRPCInternal.Code, @@ -942,6 +940,31 @@ func handleGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (i return reply, nil } +func handleGenerateToAddress(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + c := cmd.(*btcjson.GenerateToAddressCmd) + + addr, err := btcutil.DecodeAddress(c.Address, nil) + if err != nil { + return nil, err + } + + before := s.cfg.CPUMiner.MiningAddrs[:] + s.cfg.CPUMiner.MiningAddrs = []btcutil.Address{addr} + + reply, err := generate(s, uint32(c.NumBlocks)) + + s.cfg.CPUMiner.MiningAddrs = before + + return reply, err +} + +// handleGenerate handles generate commands. +func handleGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { + c := cmd.(*btcjson.GenerateCmd) + + return generate(s, c.NumBlocks) +} + // handleGetAddedNodeInfo handles getaddednodeinfo commands. func handleGetAddedNodeInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GetAddedNodeInfoCmd) diff --git a/rpcserverhelp.go b/rpcserverhelp.go index 71f96e99fd..4118fc7c3d 100644 --- a/rpcserverhelp.go +++ b/rpcserverhelp.go @@ -132,6 +132,14 @@ var helpDescsEnUS = map[string]string{ "generate-numblocks": "Number of blocks to generate", "generate--result0": "The hashes, in order, of blocks generated by the call", + // GenerateToAddressCmd help + "generatetoaddress--synopsis": "Generates a set number of blocks (simnet or regtest only), paying the block reward to the specified address and returns a JSON\n" + + " array of their hashes.", + "generatetoaddress-numblocks": "Number of blocks to generate", + "generatetoaddress-address": "The address to send the newly generated bitcoin", + "generatetoaddress-maxtries": "How many iterations to try", + "generatetoaddress--result0": "The hashes, in order, of blocks generated by the call", + // GetAddedNodeInfoResultAddr help. "getaddednodeinforesultaddr-address": "The ip address for this DNS entry", "getaddednodeinforesultaddr-connected": "The connection 'direction' (inbound/outbound/false)", @@ -766,6 +774,7 @@ var rpcResultTypes = map[string][]interface{}{ "decodescript": {(*btcjson.DecodeScriptResult)(nil)}, "estimatefee": {(*float64)(nil)}, "generate": {(*[]string)(nil)}, + "generatetoaddress": {(*[]string)(nil)}, "getaddednodeinfo": {(*[]string)(nil), (*[]btcjson.GetAddedNodeInfoResult)(nil)}, "getbestblock": {(*btcjson.GetBestBlockResult)(nil)}, "getbestblockhash": {(*string)(nil)}, From 7c6337182c8cadd1002f346fbcd078385c2083ff Mon Sep 17 00:00:00 2001 From: Linden <70739041+linden@users.noreply.github.com> Date: Sun, 29 Sep 2024 12:21:57 -0700 Subject: [PATCH 2/9] chore: move `config`, `params` and `log` to `internal/` packages makes it possible to import `config` into another package, `params` & `log` moved to avoid circular imports. --- btcd.go | 37 +- config.go => internal/config/config.go | 371 +++++++++--------- .../config/config_test.go | 4 +- log.go => internal/log/log.go | 76 ++-- params.go => internal/params/params.go | 42 +- rpcserver.go | 30 +- server.go | 45 ++- upgrade.go | 12 +- 8 files changed, 334 insertions(+), 283 deletions(-) rename config.go => internal/config/config.go (83%) rename config_test.go => internal/config/config_test.go (94%) rename log.go => internal/log/log.go (71%) rename params.go => internal/params/params.go (71%) diff --git a/btcd.go b/btcd.go index c7f292cbc9..36a764c453 100644 --- a/btcd.go +++ b/btcd.go @@ -17,6 +17,9 @@ import ( "github.com/btcsuite/btcd/blockchain/indexers" "github.com/btcsuite/btcd/database" + "github.com/btcsuite/btcd/internal/config" + "github.com/btcsuite/btcd/internal/log" + "github.com/btcsuite/btcd/internal/params" "github.com/btcsuite/btcd/limits" "github.com/btcsuite/btcd/ossec" ) @@ -29,13 +32,23 @@ const ( ) var ( - cfg *config + cfg *config.Config ) // winServiceMain is only invoked on Windows. It detects when btcd is running // as a service and reacts accordingly. var winServiceMain func() (bool, error) +var ( + btcdLog = log.BtcdLog + srvrLog = log.SrvrLog + rpcsLog = log.RpcsLog + peerLog = log.PeerLog + txmpLog = log.TxmpLog + indxLog = log.IndxLog + amgrLog = log.AmgrLog +) + // btcdMain is the real main function for btcd. It is necessary to work around // the fact that deferred functions do not run when os.Exit() is called. The // optional serverChan parameter is mainly used by the service code to be @@ -44,14 +57,14 @@ var winServiceMain func() (bool, error) func btcdMain(serverChan chan<- *server) error { // Load configuration and parse command line. This function also // initializes logging and configures it accordingly. - tcfg, _, err := loadConfig() + tcfg, _, err := config.LoadConfig(version()) if err != nil { return err } cfg = tcfg defer func() { - if logRotator != nil { - logRotator.Close() + if log.LogRotator != nil { + log.LogRotator.Close() } }() @@ -251,7 +264,7 @@ func btcdMain(serverChan chan<- *server) error { // Create server and start it. server, err := newServer(cfg.Listeners, cfg.AgentBlacklist, - cfg.AgentWhitelist, db, activeNetParams.Params, interrupt) + cfg.AgentWhitelist, db, params.ActiveNetParams.Params, interrupt) if err != nil { // TODO: this logging could do with some beautifying. btcdLog.Errorf("Unable to start server on %v: %v", @@ -376,7 +389,7 @@ func loadBlockDB() (database.DB, error) { removeRegressionDB(dbPath) btcdLog.Infof("Loading block database from '%s'", dbPath) - db, err := database.Open(cfg.DbType, dbPath, activeNetParams.Net) + db, err := database.Open(cfg.DbType, dbPath, params.ActiveNetParams.Net) if err != nil { // Return the error if it's not because the database doesn't // exist. @@ -391,7 +404,7 @@ func loadBlockDB() (database.DB, error) { if err != nil { return nil, err } - db, err = database.Create(cfg.DbType, dbPath, activeNetParams.Net) + db, err = database.Create(cfg.DbType, dbPath, params.ActiveNetParams.Net) if err != nil { return nil, err } @@ -456,3 +469,13 @@ func main() { os.Exit(1) } } + +// fileExists reports whether the named file or directory exists. +func fileExists(name string) bool { + if _, err := os.Stat(name); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} diff --git a/config.go b/internal/config/config.go similarity index 83% rename from config.go rename to internal/config/config.go index 9bbce7f69a..ccea1b98fe 100644 --- a/config.go +++ b/internal/config/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 config import ( "bufio" @@ -28,6 +28,8 @@ import ( "github.com/btcsuite/btcd/connmgr" "github.com/btcsuite/btcd/database" _ "github.com/btcsuite/btcd/database/ffldb" + "github.com/btcsuite/btcd/internal/log" + "github.com/btcsuite/btcd/internal/params" "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/peer" "github.com/btcsuite/btcd/wire" @@ -36,48 +38,48 @@ import ( ) const ( - defaultConfigFilename = "btcd.conf" - defaultDataDirname = "data" - defaultLogLevel = "info" - defaultLogDirname = "logs" - defaultLogFilename = "btcd.log" - defaultMaxPeers = 125 - defaultBanDuration = time.Hour * 24 - defaultBanThreshold = 100 - defaultConnectTimeout = time.Second * 30 - defaultMaxRPCClients = 10 - defaultMaxRPCWebsockets = 25 - defaultMaxRPCConcurrentReqs = 20 - defaultDbType = "ffldb" - defaultFreeTxRelayLimit = 15.0 - defaultTrickleInterval = peer.DefaultTrickleInterval - defaultBlockMinSize = 0 - defaultBlockMaxSize = 750000 - defaultBlockMinWeight = 0 - defaultBlockMaxWeight = 3000000 - blockMaxSizeMin = 1000 - blockMaxSizeMax = blockchain.MaxBlockBaseSize - 1000 - blockMaxWeightMin = 4000 - blockMaxWeightMax = blockchain.MaxBlockWeight - 4000 - defaultGenerate = false - defaultMaxOrphanTransactions = 100 - defaultMaxOrphanTxSize = 100000 - defaultSigCacheMaxSize = 100000 - defaultUtxoCacheMaxSizeMiB = 250 - sampleConfigFilename = "sample-btcd.conf" - defaultTxIndex = false - defaultAddrIndex = false - pruneMinSize = 1536 + DefaultConfigFilename = "btcd.conf" + DefaultDataDirname = "data" + DefaultLogLevel = "info" + DefaultLogDirname = "logs" + DefaultLogFilename = "btcd.log" + DefaultMaxPeers = 125 + DefaultBanDuration = time.Hour * 24 + DefaultBanThreshold = 100 + DefaultConnectTimeout = time.Second * 30 + DefaultMaxRPCClients = 10 + DefaultMaxRPCWebsockets = 25 + DefaultMaxRPCConcurrentReqs = 20 + DefaultDbType = "ffldb" + DefaultFreeTxRelayLimit = 15.0 + DefaultTrickleInterval = peer.DefaultTrickleInterval + DefaultBlockMinSize = 0 + DefaultBlockMaxSize = 750000 + DefaultBlockMinWeight = 0 + DefaultBlockMaxWeight = 3000000 + BlockMaxSizeMin = 1000 + BlockMaxSizeMax = blockchain.MaxBlockBaseSize - 1000 + BlockMaxWeightMin = 4000 + BlockMaxWeightMax = blockchain.MaxBlockWeight - 4000 + DefaultGenerate = false + DefaultMaxOrphanTransactions = 100 + DefaultMaxOrphanTxSize = 100000 + DefaultSigCacheMaxSize = 100000 + DefaultUtxoCacheMaxSizeMiB = 250 + SampleConfigFilename = "sample-btcd.conf" + DefaultTxIndex = false + DefaultAddrIndex = false + PruneMinSize = 1536 ) var ( - defaultHomeDir = btcutil.AppDataDir("btcd", false) - defaultConfigFile = filepath.Join(defaultHomeDir, defaultConfigFilename) - defaultDataDir = filepath.Join(defaultHomeDir, defaultDataDirname) - knownDbTypes = database.SupportedDrivers() - defaultRPCKeyFile = filepath.Join(defaultHomeDir, "rpc.key") - defaultRPCCertFile = filepath.Join(defaultHomeDir, "rpc.cert") - defaultLogDir = filepath.Join(defaultHomeDir, defaultLogDirname) + DefaultHomeDir = btcutil.AppDataDir("btcd", false) + DefaultConfigFile = filepath.Join(DefaultHomeDir, DefaultConfigFilename) + DefaultDataDir = filepath.Join(DefaultHomeDir, DefaultDataDirname) + KnownDbTypes = database.SupportedDrivers() + DefaultRPCKeyFile = filepath.Join(DefaultHomeDir, "rpc.key") + DefaultRPCCertFile = filepath.Join(DefaultHomeDir, "rpc.cert") + DefaultLogDir = filepath.Join(DefaultHomeDir, DefaultLogDirname) ) // runServiceCommand is only set to a real function on Windows. It is used @@ -93,11 +95,7 @@ func minUint32(a, b uint32) uint32 { return b } -// config defines the configuration options for btcd. -// -// See loadConfig for details on the configuration load process. -type config struct { - AddCheckpoints []string `long:"addcheckpoint" description:"Add a custom checkpoint. Format: ':'"` +type Config struct { AddPeers []string `short:"a" long:"addpeer" description:"Add a peer to connect with at startup"` AddrIndex bool `long:"addrindex" description:"Maintain a full address-based transaction index which makes the searchrawtransactions RPC available"` AgentBlacklist []string `long:"agentblacklist" description:"A comma separated list of user-agent substrings which will cause btcd to reject any peers whose user-agent contains any of the blacklisted substrings."` @@ -127,8 +125,6 @@ type config struct { LogDir string `long:"logdir" description:"Directory to log output."` MaxOrphanTxs int `long:"maxorphantx" description:"Max number of orphan transactions to keep in memory"` MaxPeers int `long:"maxpeers" description:"Max number of inbound and outbound peers"` - MiningAddrs []string `long:"miningaddr" description:"Add the specified payment address to the list of addresses to use for generated blocks -- At least one address is required if the generate option is set"` - MinRelayTxFee float64 `long:"minrelaytxfee" description:"The minimum transaction fee in BTC/kB to be considered a non-zero fee."` DisableBanning bool `long:"nobanning" description:"Disable banning of misbehaving peers"` NoCFilters bool `long:"nocfilters" description:"Disable committed filtering (CF) support"` DisableCheckpoints bool `long:"nocheckpoints" description:"Disable built-in checkpoints. Don't do this unless you know what you're doing."` @@ -177,14 +173,60 @@ type config struct { UserAgentComments []string `long:"uacomment" description:"Comment to add to the user agent -- See BIP 14 for more information."` Upnp bool `long:"upnp" description:"Use UPnP to map our listening port outside of NAT"` ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` - Whitelists []string `long:"whitelist" description:"Add an IP network or IP that will not be banned. (eg. 192.168.1.0/24 or ::1)"` - lookup func(string) ([]net.IP, error) - oniondial func(string, string, time.Duration) (net.Conn, error) - dial func(string, string, time.Duration) (net.Conn, error) - addCheckpoints []chaincfg.Checkpoint - miningAddrs []btcutil.Address - minRelayTxFee btcutil.Amount - whitelists []*net.IPNet + + AddCheckpoints []chaincfg.Checkpoint + MiningAddrs []btcutil.Address + MinRelayTxFee btcutil.Amount + Whitelists []*net.IPNet + + lookup func(string) ([]net.IP, error) + oniondial func(string, string, time.Duration) (net.Conn, error) + dial func(string, string, time.Duration) (net.Conn, error) +} + +func (cfg *Config) ChangeRoot(root string) { + cfg.LogDir = filepath.Join(root, "data") + cfg.RPCKey = filepath.Join(root, "rpc.key") + cfg.DataDir = filepath.Join(root, "data") + cfg.RPCCert = filepath.Join(root, "rpc.cert") + cfg.ConfigFile = filepath.Join(root, "btcd.conf") +} + +// Dial connects to the address on the named network using the appropriate +// dial function depending on the address and configuration options. For +// example, .onion addresses will be dialed using the onion specific proxy if +// one was specified, but will otherwise use the normal dial function (which +// could itself use a proxy or not). +func (cfg *Config) Dial(addr net.Addr) (net.Conn, error) { + if strings.Contains(addr.String(), ".onion:") { + return cfg.oniondial(addr.Network(), addr.String(), + DefaultConnectTimeout) + } + return cfg.dial(addr.Network(), addr.String(), DefaultConnectTimeout) +} + +// Lookup resolves the IP of the given host using the correct DNS lookup +// function depending on the configuration options. For example, addresses will +// be resolved using tor when the --proxy flag was specified unless --noonion +// was also specified in which case the normal system DNS resolver will be used. +// +// Any attempt to resolve a tor address (.onion) will return an error since they +// are not intended to be resolved outside of the tor proxy. +func (cfg *Config) Lookup(host string) ([]net.IP, error) { + if strings.HasSuffix(host, ".onion") { + return nil, fmt.Errorf("attempt to resolve tor address %s", host) + } + + return cfg.lookup(host) +} + +type config struct { + Config + + AddCheckpoints []string `long:"addcheckpoint" description:"Add a custom checkpoint. Format: ':'"` + MiningAddrs []string `long:"miningaddr" description:"Add the specified payment address to the list of addresses to use for generated blocks -- At least one address is required if the generate option is set"` + MinRelayTxFee float64 `long:"minrelaytxfee" description:"The minimum transaction fee in BTC/kB to be considered a non-zero fee."` + Whitelists []string `long:"whitelist" description:"Add an IP network or IP that will not be banned. (eg. 192.168.1.0/24 or ::1)"` } // serviceOptions defines the configuration options for the daemon as a service on @@ -198,7 +240,7 @@ type serviceOptions struct { func cleanAndExpandPath(path string) string { // Expand initial ~ to OS specific home directory. if strings.HasPrefix(path, "~") { - homeDir := filepath.Dir(defaultHomeDir) + homeDir := filepath.Dir(DefaultHomeDir) path = strings.Replace(path, "~", homeDir, 1) } @@ -226,12 +268,12 @@ func validLogLevel(logLevel string) bool { return false } -// supportedSubsystems returns a sorted slice of the supported subsystems for +// SupportedSubsystems returns a sorted slice of the supported subsystems for // logging purposes. -func supportedSubsystems() []string { +func SupportedSubsystems() []string { // Convert the subsystemLoggers map keys to a slice. - subsystems := make([]string, 0, len(subsystemLoggers)) - for subsysID := range subsystemLoggers { + subsystems := make([]string, 0, len(log.SubsystemLoggers)) + for subsysID := range log.SubsystemLoggers { subsystems = append(subsystems, subsysID) } @@ -240,10 +282,10 @@ func supportedSubsystems() []string { return subsystems } -// parseAndSetDebugLevels attempts to parse the specified debug level and set +// ParseAndSetDebugLevels attempts to parse the specified debug level and set // the levels accordingly. An appropriate error is returned if anything is // invalid. -func parseAndSetDebugLevels(debugLevel string) error { +func ParseAndSetDebugLevels(debugLevel string) error { // When the specified string doesn't have any delimiters, treat it as // the log level for all subsystems. if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") { @@ -254,7 +296,7 @@ func parseAndSetDebugLevels(debugLevel string) error { } // Change the logging level for all subsystems. - setLogLevels(debugLevel) + log.SetLogLevels(debugLevel) return nil } @@ -273,10 +315,10 @@ func parseAndSetDebugLevels(debugLevel string) error { subsysID, logLevel := fields[0], fields[1] // Validate subsystem. - if _, exists := subsystemLoggers[subsysID]; !exists { + if _, exists := log.SubsystemLoggers[subsysID]; !exists { str := "The specified subsystem [%v] is invalid -- " + "supported subsystems %v" - return fmt.Errorf(str, subsysID, supportedSubsystems()) + return fmt.Errorf(str, subsysID, SupportedSubsystems()) } // Validate log level. @@ -285,7 +327,7 @@ func parseAndSetDebugLevels(debugLevel string) error { return fmt.Errorf(str, logLevel) } - setLogLevel(subsysID, logLevel) + log.SetLogLevel(subsysID, logLevel) } return nil @@ -293,7 +335,7 @@ func parseAndSetDebugLevels(debugLevel string) error { // validDbType returns whether or not dbType is a supported database type. func validDbType(dbType string) bool { - for _, knownType := range knownDbTypes { + for _, knownType := range KnownDbTypes { if dbType == knownType { return true } @@ -316,9 +358,9 @@ func removeDuplicateAddresses(addrs []string) []string { return result } -// normalizeAddress returns addr with the passed default port appended if +// NormalizeAddress returns addr with the passed default port appended if // there is not already a port specified. -func normalizeAddress(addr, defaultPort string) string { +func NormalizeAddress(addr, defaultPort string) string { _, _, err := net.SplitHostPort(addr) if err != nil { return net.JoinHostPort(addr, defaultPort) @@ -330,7 +372,7 @@ func normalizeAddress(addr, defaultPort string) string { // normalized with the given default port, and all duplicates removed. func normalizeAddresses(addrs []string, defaultPort string) []string { for i, addr := range addrs { - addrs[i] = normalizeAddress(addr, defaultPort) + addrs[i] = NormalizeAddress(addr, defaultPort) } return removeDuplicateAddresses(addrs) @@ -403,7 +445,39 @@ func newConfigParser(cfg *config, so *serviceOptions, options flags.Options) *fl return parser } -// loadConfig initializes and parses the config using a config file and command +func NewDefaultConfig() Config { + return Config{ + ConfigFile: DefaultConfigFile, + DebugLevel: DefaultLogLevel, + MaxPeers: DefaultMaxPeers, + BanDuration: DefaultBanDuration, + BanThreshold: DefaultBanThreshold, + RPCMaxClients: DefaultMaxRPCClients, + RPCMaxWebsockets: DefaultMaxRPCWebsockets, + RPCMaxConcurrentReqs: DefaultMaxRPCConcurrentReqs, + DataDir: DefaultDataDir, + LogDir: DefaultLogDir, + DbType: DefaultDbType, + RPCKey: DefaultRPCKeyFile, + RPCCert: DefaultRPCCertFile, + FreeTxRelayLimit: DefaultFreeTxRelayLimit, + TrickleInterval: DefaultTrickleInterval, + BlockMinSize: DefaultBlockMinSize, + BlockMaxSize: DefaultBlockMaxSize, + BlockMinWeight: DefaultBlockMinWeight, + BlockMaxWeight: DefaultBlockMaxWeight, + BlockPrioritySize: mempool.DefaultBlockPrioritySize, + MaxOrphanTxs: DefaultMaxOrphanTransactions, + SigCacheMaxSize: DefaultSigCacheMaxSize, + UtxoCacheMaxSizeMiB: DefaultUtxoCacheMaxSizeMiB, + Generate: DefaultGenerate, + TxIndex: DefaultTxIndex, + AddrIndex: DefaultAddrIndex, + MinRelayTxFee: mempool.DefaultMinRelayTxFee, + } +} + +// LoadConfig initializes and parses the config using a config file and command // line options. // // The configuration proceeds as follows: @@ -415,36 +489,11 @@ func newConfigParser(cfg *config, so *serviceOptions, options flags.Options) *fl // The above results in btcd 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() (*config, []string, error) { +func LoadConfig(version string) (*Config, []string, error) { // Default config. cfg := config{ - ConfigFile: defaultConfigFile, - DebugLevel: defaultLogLevel, - MaxPeers: defaultMaxPeers, - BanDuration: defaultBanDuration, - BanThreshold: defaultBanThreshold, - RPCMaxClients: defaultMaxRPCClients, - RPCMaxWebsockets: defaultMaxRPCWebsockets, - RPCMaxConcurrentReqs: defaultMaxRPCConcurrentReqs, - DataDir: defaultDataDir, - LogDir: defaultLogDir, - DbType: defaultDbType, - RPCKey: defaultRPCKeyFile, - RPCCert: defaultRPCCertFile, - MinRelayTxFee: mempool.DefaultMinRelayTxFee.ToBTC(), - FreeTxRelayLimit: defaultFreeTxRelayLimit, - TrickleInterval: defaultTrickleInterval, - BlockMinSize: defaultBlockMinSize, - BlockMaxSize: defaultBlockMaxSize, - BlockMinWeight: defaultBlockMinWeight, - BlockMaxWeight: defaultBlockMaxWeight, - BlockPrioritySize: mempool.DefaultBlockPrioritySize, - MaxOrphanTxs: defaultMaxOrphanTransactions, - SigCacheMaxSize: defaultSigCacheMaxSize, - UtxoCacheMaxSizeMiB: defaultUtxoCacheMaxSizeMiB, - Generate: defaultGenerate, - TxIndex: defaultTxIndex, - AddrIndex: defaultAddrIndex, + Config: NewDefaultConfig(), + MinRelayTxFee: mempool.DefaultMinRelayTxFee.ToBTC(), } // Service options which are only added on Windows. @@ -469,7 +518,7 @@ func loadConfig() (*config, []string, error) { appName = strings.TrimSuffix(appName, filepath.Ext(appName)) usageMessage := fmt.Sprintf("Use %s -h to show usage", appName) if preCfg.ShowVersion { - fmt.Println(appName, "version", version()) + fmt.Println(appName, "version", version) os.Exit(0) } @@ -488,7 +537,7 @@ func loadConfig() (*config, []string, error) { var configFileError error parser := newConfigParser(&cfg, &serviceOpts, flags.Default) if !(preCfg.RegressionTest || preCfg.SimNet || preCfg.SigNet) || - preCfg.ConfigFile != defaultConfigFile { + preCfg.ConfigFile != DefaultConfigFile { if _, err := os.Stat(preCfg.ConfigFile); os.IsNotExist(err) { err := createDefaultConfigFile(preCfg.ConfigFile) @@ -526,7 +575,7 @@ func loadConfig() (*config, []string, error) { // Create the home directory if it doesn't already exist. funcName := "loadConfig" - err = os.MkdirAll(defaultHomeDir, 0700) + err = os.MkdirAll(DefaultHomeDir, 0700) if err != nil { // Show a nicer error message if it's because a symlink is // linked to a directory that does not exist (probably because @@ -550,21 +599,21 @@ func loadConfig() (*config, []string, error) { // while we're at it if cfg.TestNet3 { numNets++ - activeNetParams = &testNet3Params + params.ActiveNetParams = ¶ms.TestNet3Params } if cfg.RegressionTest { numNets++ - activeNetParams = ®ressionNetParams + params.ActiveNetParams = ¶ms.RegressionNetParams } if cfg.SimNet { numNets++ // Also disable dns seeding on the simulation test network. - activeNetParams = &simNetParams + params.ActiveNetParams = ¶ms.SimNetParams cfg.DisableDNSSeed = true } if cfg.SigNet { numNets++ - activeNetParams = &sigNetParams + params.ActiveNetParams = ¶ms.SigNetParams // Let the user overwrite the default signet parameters. The // challenge defines the actual signet network to join and the @@ -599,7 +648,7 @@ func loadConfig() (*config, []string, error) { chainParams := chaincfg.CustomSignetParams( sigNetChallenge, sigNetSeeds, ) - activeNetParams.Params = &chainParams + params.ActiveNetParams.Params = &chainParams } if numNets > 1 { str := "%s: The testnet, regtest, segnet, signet and simnet " + @@ -613,7 +662,7 @@ func loadConfig() (*config, []string, error) { // If mainnet is active, then we won't allow the stall handler to be // disabled. - if activeNetParams.Params.Net == wire.MainNet && cfg.DisableStallHandler { + if params.ActiveNetParams.Params.Net == wire.MainNet && cfg.DisableStallHandler { str := "%s: stall handler cannot be disabled on mainnet" err := fmt.Errorf(str, funcName) fmt.Fprintln(os.Stderr, err) @@ -625,7 +674,7 @@ func loadConfig() (*config, []string, error) { // according to the default of the active network. The set // configuration value takes precedence over the default value for the // selected network. - relayNonStd := activeNetParams.RelayNonStdTxs + relayNonStd := params.ActiveNetParams.RelayNonStdTxs switch { case cfg.RelayNonStd && cfg.RejectNonStd: str := "%s: rejectnonstd and relaynonstd cannot be used " + @@ -648,25 +697,25 @@ func loadConfig() (*config, []string, error) { // means each individual piece of serialized data does not have to // worry about changing names per network and such. cfg.DataDir = cleanAndExpandPath(cfg.DataDir) - cfg.DataDir = filepath.Join(cfg.DataDir, netName(activeNetParams)) + cfg.DataDir = filepath.Join(cfg.DataDir, params.NetName(params.ActiveNetParams)) // Append the network type to the log directory so it is "namespaced" // per network in the same fashion as the data directory. cfg.LogDir = cleanAndExpandPath(cfg.LogDir) - cfg.LogDir = filepath.Join(cfg.LogDir, netName(activeNetParams)) + cfg.LogDir = filepath.Join(cfg.LogDir, params.NetName(params.ActiveNetParams)) // Special show command to list supported subsystems and exit. if cfg.DebugLevel == "show" { - fmt.Println("Supported subsystems", supportedSubsystems()) + fmt.Println("Supported subsystems", SupportedSubsystems()) os.Exit(0) } // Initialize log rotation. After log rotation has been initialized, the // logger variables may be used. - initLogRotator(filepath.Join(cfg.LogDir, defaultLogFilename)) + log.InitLogRotator(filepath.Join(cfg.LogDir, DefaultLogFilename)) // Parse, validate, and set debug log level(s). - if err := parseAndSetDebugLevels(cfg.DebugLevel); err != nil { + if err := ParseAndSetDebugLevels(cfg.DebugLevel); err != nil { err := fmt.Errorf("%s: %v", funcName, err.Error()) fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, usageMessage) @@ -677,7 +726,7 @@ func loadConfig() (*config, []string, error) { if !validDbType(cfg.DbType) { str := "%s: The specified database type [%v] is invalid -- " + "supported types %v" - err := fmt.Errorf(str, funcName, cfg.DbType, knownDbTypes) + err := fmt.Errorf(str, funcName, cfg.DbType, KnownDbTypes) fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, usageMessage) return nil, nil, err @@ -707,7 +756,7 @@ func loadConfig() (*config, []string, error) { // Validate any given whitelisted IP addresses and networks. if len(cfg.Whitelists) > 0 { var ip net.IP - cfg.whitelists = make([]*net.IPNet, 0, len(cfg.Whitelists)) + cfg.Config.Whitelists = make([]*net.IPNet, 0, len(cfg.Whitelists)) for _, addr := range cfg.Whitelists { _, ipnet, err := net.ParseCIDR(addr) @@ -732,7 +781,7 @@ func loadConfig() (*config, []string, error) { Mask: net.CIDRMask(bits, bits), } } - cfg.whitelists = append(cfg.whitelists, ipnet) + cfg.Config.Whitelists = append(cfg.Config.Whitelists, ipnet) } } @@ -762,7 +811,7 @@ func loadConfig() (*config, []string, error) { // we are to connect to. if len(cfg.Listeners) == 0 { cfg.Listeners = []string{ - net.JoinHostPort("", activeNetParams.DefaultPort), + net.JoinHostPort("", params.ActiveNetParams.DefaultPort), } } @@ -793,7 +842,7 @@ func loadConfig() (*config, []string, error) { } if cfg.DisableRPC { - btcdLog.Infof("RPC service is disabled") + log.BtcdLog.Infof("RPC service is disabled") } // Default RPC to listen on localhost only. @@ -804,7 +853,7 @@ func loadConfig() (*config, []string, error) { } cfg.RPCListeners = make([]string, 0, len(addrs)) for _, addr := range addrs { - addr = net.JoinHostPort(addr, activeNetParams.rpcPort) + addr = net.JoinHostPort(addr, params.ActiveNetParams.RPCPort) cfg.RPCListeners = append(cfg.RPCListeners, addr) } } @@ -819,7 +868,7 @@ func loadConfig() (*config, []string, error) { } // Validate the minrelaytxfee. - cfg.minRelayTxFee, err = btcutil.NewAmount(cfg.MinRelayTxFee) + cfg.Config.MinRelayTxFee, err = btcutil.NewAmount(cfg.MinRelayTxFee) if err != nil { str := "%s: invalid minrelaytxfee: %v" err := fmt.Errorf(str, funcName, err) @@ -829,26 +878,26 @@ func loadConfig() (*config, []string, error) { } // Limit the max block size to a sane value. - if cfg.BlockMaxSize < blockMaxSizeMin || cfg.BlockMaxSize > - blockMaxSizeMax { + if cfg.BlockMaxSize < BlockMaxSizeMin || cfg.BlockMaxSize > + BlockMaxSizeMax { str := "%s: The blockmaxsize option must be in between %d " + "and %d -- parsed [%d]" - err := fmt.Errorf(str, funcName, blockMaxSizeMin, - blockMaxSizeMax, cfg.BlockMaxSize) + err := fmt.Errorf(str, funcName, BlockMaxSizeMin, + BlockMaxSizeMax, cfg.BlockMaxSize) fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, usageMessage) return nil, nil, err } // Limit the max block weight to a sane value. - if cfg.BlockMaxWeight < blockMaxWeightMin || - cfg.BlockMaxWeight > blockMaxWeightMax { + if cfg.BlockMaxWeight < BlockMaxWeightMin || + cfg.BlockMaxWeight > BlockMaxWeightMax { str := "%s: The blockmaxweight option must be in between %d " + "and %d -- parsed [%d]" - err := fmt.Errorf(str, funcName, blockMaxWeightMin, - blockMaxWeightMax, cfg.BlockMaxWeight) + err := fmt.Errorf(str, funcName, BlockMaxWeightMin, + BlockMaxWeightMax, cfg.BlockMaxWeight) fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, usageMessage) return nil, nil, err @@ -873,15 +922,15 @@ func loadConfig() (*config, []string, error) { // If the max block size isn't set, but the max weight is, then we'll // set the limit for the max block size to a safe limit so weight takes // precedence. - case cfg.BlockMaxSize == defaultBlockMaxSize && - cfg.BlockMaxWeight != defaultBlockMaxWeight: + case cfg.BlockMaxSize == DefaultBlockMaxSize && + cfg.BlockMaxWeight != DefaultBlockMaxWeight: cfg.BlockMaxSize = blockchain.MaxBlockBaseSize - 1000 // If the max block weight isn't set, but the block size is, then we'll // scale the set weight accordingly based on the max block size value. - case cfg.BlockMaxSize != defaultBlockMaxSize && - cfg.BlockMaxWeight == defaultBlockMaxWeight: + case cfg.BlockMaxSize != DefaultBlockMaxSize && + cfg.BlockMaxWeight == DefaultBlockMaxWeight: cfg.BlockMaxWeight = cfg.BlockMaxSize * blockchain.WitnessScaleFactor } @@ -931,9 +980,9 @@ func loadConfig() (*config, []string, error) { } // Check mining addresses are valid and saved parsed versions. - cfg.miningAddrs = make([]btcutil.Address, 0, len(cfg.MiningAddrs)) + cfg.Config.MiningAddrs = make([]btcutil.Address, 0, len(cfg.MiningAddrs)) for _, strAddr := range cfg.MiningAddrs { - addr, err := btcutil.DecodeAddress(strAddr, activeNetParams.Params) + addr, err := btcutil.DecodeAddress(strAddr, params.ActiveNetParams.Params) if err != nil { str := "%s: mining address '%s' failed to decode: %v" err := fmt.Errorf(str, funcName, strAddr, err) @@ -941,14 +990,14 @@ func loadConfig() (*config, []string, error) { fmt.Fprintln(os.Stderr, usageMessage) return nil, nil, err } - if !addr.IsForNet(activeNetParams.Params) { + if !addr.IsForNet(params.ActiveNetParams.Params) { str := "%s: mining address '%s' is on the wrong network" err := fmt.Errorf(str, funcName, strAddr) fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, usageMessage) return nil, nil, err } - cfg.miningAddrs = append(cfg.miningAddrs, addr) + cfg.Config.MiningAddrs = append(cfg.Config.MiningAddrs, addr) } // Ensure there is at least one mining address when the generate flag is @@ -965,12 +1014,12 @@ func loadConfig() (*config, []string, error) { // Add default port to all listener addresses if needed and remove // duplicate addresses. cfg.Listeners = normalizeAddresses(cfg.Listeners, - activeNetParams.DefaultPort) + params.ActiveNetParams.DefaultPort) // Add default port to all rpc listener addresses if needed and remove // duplicate addresses. cfg.RPCListeners = normalizeAddresses(cfg.RPCListeners, - activeNetParams.rpcPort) + params.ActiveNetParams.RPCPort) // Only allow TLS to be disabled if the RPC is bound to localhost // addresses. @@ -1005,9 +1054,9 @@ func loadConfig() (*config, []string, error) { // Add default port to all added peer addresses if needed and remove // duplicate addresses. cfg.AddPeers = normalizeAddresses(cfg.AddPeers, - activeNetParams.DefaultPort) + params.ActiveNetParams.DefaultPort) cfg.ConnectPeers = normalizeAddresses(cfg.ConnectPeers, - activeNetParams.DefaultPort) + params.ActiveNetParams.DefaultPort) // --noonion and --onion do not mix. if cfg.NoOnion && cfg.OnionProxy != "" { @@ -1019,7 +1068,7 @@ func loadConfig() (*config, []string, error) { } // Check the checkpoints for syntax errors. - cfg.addCheckpoints, err = parseCheckpoints(cfg.AddCheckpoints) + cfg.Config.AddCheckpoints, err = parseCheckpoints(cfg.AddCheckpoints) if err != nil { str := "%s: Error parsing checkpoints: %v" err := fmt.Errorf(str, funcName, err) @@ -1142,9 +1191,9 @@ func loadConfig() (*config, []string, error) { } } - if cfg.Prune != 0 && cfg.Prune < pruneMinSize { + if cfg.Prune != 0 && cfg.Prune < PruneMinSize { err := fmt.Errorf("%s: the minimum value for --prune is %d. Got %d", - funcName, pruneMinSize, cfg.Prune) + funcName, PruneMinSize, cfg.Prune) fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, usageMessage) return nil, nil, err @@ -1170,10 +1219,10 @@ func loadConfig() (*config, []string, error) { // done. This prevents the warning on help messages and invalid // options. Note this should go directly before the return. if configFileError != nil { - btcdLog.Warnf("%v", configFileError) + log.BtcdLog.Warnf("%v", configFileError) } - return &cfg, remainingArgs, nil + return &cfg.Config, remainingArgs, nil } // createDefaultConfig copies the file sample-btcd.conf to the given destination path, @@ -1190,7 +1239,7 @@ func createDefaultConfigFile(destinationPath string) error { if err != nil { return err } - sampleConfigPath := filepath.Join(path, sampleConfigFilename) + sampleConfigPath := filepath.Join(path, SampleConfigFilename) // We generate a random user and password randomBytes := make([]byte, 20) @@ -1242,31 +1291,3 @@ func createDefaultConfigFile(destinationPath string) error { return nil } - -// btcdDial connects to the address on the named network using the appropriate -// dial function depending on the address and configuration options. For -// example, .onion addresses will be dialed using the onion specific proxy if -// one was specified, but will otherwise use the normal dial function (which -// could itself use a proxy or not). -func btcdDial(addr net.Addr) (net.Conn, error) { - if strings.Contains(addr.String(), ".onion:") { - return cfg.oniondial(addr.Network(), addr.String(), - defaultConnectTimeout) - } - return cfg.dial(addr.Network(), addr.String(), defaultConnectTimeout) -} - -// btcdLookup resolves the IP of the given host using the correct DNS lookup -// function depending on the configuration options. For example, addresses will -// be resolved using tor when the --proxy flag was specified unless --noonion -// was also specified in which case the normal system DNS resolver will be used. -// -// Any attempt to resolve a tor address (.onion) will return an error since they -// are not intended to be resolved outside of the tor proxy. -func btcdLookup(host string) ([]net.IP, error) { - if strings.HasSuffix(host, ".onion") { - return nil, fmt.Errorf("attempt to resolve tor address %s", host) - } - - return cfg.lookup(host) -} diff --git a/config_test.go b/internal/config/config_test.go similarity index 94% rename from config_test.go rename to internal/config/config_test.go index 42a0cd4b90..7b4d6e28b7 100644 --- a/config_test.go +++ b/internal/config/config_test.go @@ -1,4 +1,4 @@ -package main +package config import ( "os" @@ -19,7 +19,7 @@ func TestCreateDefaultConfigFile(t *testing.T) { if !ok { t.Fatalf("Failed finding config file path") } - sampleConfigFile := filepath.Join(filepath.Dir(path), "sample-btcd.conf") + sampleConfigFile := filepath.Join(filepath.Dir(path), "../..", "sample-btcd.conf") // Setup a temporary directory tmpDir, err := os.MkdirTemp("", "btcd") diff --git a/log.go b/internal/log/log.go similarity index 71% rename from log.go rename to internal/log/log.go index 5707d7c23a..9d82cc95f5 100644 --- a/log.go +++ b/internal/log/log.go @@ -3,7 +3,7 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package main +package log import ( "fmt" @@ -32,7 +32,7 @@ type logWriter struct{} func (logWriter) Write(p []byte) (n int, err error) { os.Stdout.Write(p) - logRotator.Write(p) + LogRotator.Write(p) return len(p), nil } @@ -50,65 +50,65 @@ var ( // or data races and/or nil pointer dereferences will occur. backendLog = btclog.NewBackend(logWriter{}) - // logRotator is one of the logging outputs. It should be closed on + // LogRotator is one of the logging outputs. It should be closed on // application shutdown. - logRotator *rotator.Rotator + LogRotator *rotator.Rotator adxrLog = backendLog.Logger("ADXR") - amgrLog = backendLog.Logger("AMGR") + AmgrLog = backendLog.Logger("AMGR") cmgrLog = backendLog.Logger("CMGR") bcdbLog = backendLog.Logger("BCDB") - btcdLog = backendLog.Logger("BTCD") + BtcdLog = backendLog.Logger("BTCD") chanLog = backendLog.Logger("CHAN") discLog = backendLog.Logger("DISC") - indxLog = backendLog.Logger("INDX") + IndxLog = backendLog.Logger("INDX") minrLog = backendLog.Logger("MINR") - peerLog = backendLog.Logger("PEER") - rpcsLog = backendLog.Logger("RPCS") + PeerLog = backendLog.Logger("PEER") + RpcsLog = backendLog.Logger("RPCS") scrpLog = backendLog.Logger("SCRP") - srvrLog = backendLog.Logger("SRVR") + SrvrLog = backendLog.Logger("SRVR") syncLog = backendLog.Logger("SYNC") - txmpLog = backendLog.Logger("TXMP") + TxmpLog = backendLog.Logger("TXMP") ) // Initialize package-global logger variables. func init() { - addrmgr.UseLogger(amgrLog) + addrmgr.UseLogger(AmgrLog) connmgr.UseLogger(cmgrLog) database.UseLogger(bcdbLog) blockchain.UseLogger(chanLog) - indexers.UseLogger(indxLog) + indexers.UseLogger(IndxLog) mining.UseLogger(minrLog) cpuminer.UseLogger(minrLog) - peer.UseLogger(peerLog) + peer.UseLogger(PeerLog) txscript.UseLogger(scrpLog) netsync.UseLogger(syncLog) - mempool.UseLogger(txmpLog) + mempool.UseLogger(TxmpLog) } -// subsystemLoggers maps each subsystem identifier to its associated logger. -var subsystemLoggers = map[string]btclog.Logger{ +// SubsystemLoggers maps each subsystem identifier to its associated logger. +var SubsystemLoggers = map[string]btclog.Logger{ "ADXR": adxrLog, - "AMGR": amgrLog, + "AMGR": AmgrLog, "CMGR": cmgrLog, "BCDB": bcdbLog, - "BTCD": btcdLog, + "BTCD": BtcdLog, "CHAN": chanLog, "DISC": discLog, - "INDX": indxLog, + "INDX": IndxLog, "MINR": minrLog, - "PEER": peerLog, - "RPCS": rpcsLog, + "PEER": PeerLog, + "RPCS": RpcsLog, "SCRP": scrpLog, - "SRVR": srvrLog, + "SRVR": SrvrLog, "SYNC": syncLog, - "TXMP": txmpLog, + "TXMP": TxmpLog, } -// initLogRotator initializes the logging rotater to write logs to logFile and +// InitLogRotator initializes the logging rotater to write logs to logFile and // create roll files in the same directory. It must be called before the // package-global log rotater variables are used. -func initLogRotator(logFile string) { +func InitLogRotator(logFile string) { logDir, _ := filepath.Split(logFile) err := os.MkdirAll(logDir, 0700) if err != nil { @@ -121,15 +121,15 @@ func initLogRotator(logFile string) { os.Exit(1) } - logRotator = r + LogRotator = r } -// setLogLevel sets the logging level for provided subsystem. Invalid +// SetLogLevel sets the logging level for provided subsystem. Invalid // subsystems are ignored. Uninitialized subsystems are dynamically created as // needed. -func setLogLevel(subsystemID string, logLevel string) { +func SetLogLevel(subsystemID string, logLevel string) { // Ignore invalid subsystems. - logger, ok := subsystemLoggers[subsystemID] + logger, ok := SubsystemLoggers[subsystemID] if !ok { return } @@ -139,29 +139,29 @@ func setLogLevel(subsystemID string, logLevel string) { logger.SetLevel(level) } -// setLogLevels sets the log level for all subsystem loggers to the passed +// SetLogLevels sets the log level for all subsystem loggers to the passed // level. It also dynamically creates the subsystem loggers as needed, so it // can be used to initialize the logging system. -func setLogLevels(logLevel string) { +func SetLogLevels(logLevel string) { // Configure all sub-systems with the new logging level. Dynamically // create loggers as needed. - for subsystemID := range subsystemLoggers { - setLogLevel(subsystemID, logLevel) + for subsystemID := range SubsystemLoggers { + SetLogLevel(subsystemID, logLevel) } } -// directionString is a helper function that returns a string that represents +// DirectionString is a helper function that returns a string that represents // the direction of a connection (inbound or outbound). -func directionString(inbound bool) string { +func DirectionString(inbound bool) string { if inbound { return "inbound" } return "outbound" } -// pickNoun returns the singular or plural form of a noun depending +// PickNoun returns the singular or plural form of a noun depending // on the count n. -func pickNoun(n uint64, singular, plural string) string { +func PickNoun(n uint64, singular, plural string) string { if n == 1 { return singular } diff --git a/params.go b/internal/params/params.go similarity index 71% rename from params.go rename to internal/params/params.go index b4d1453dfb..f1873d5560 100644 --- a/params.go +++ b/internal/params/params.go @@ -2,67 +2,67 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package main +package params import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" ) -// activeNetParams is a pointer to the parameters specific to the +// ActiveNetParams is a pointer to the parameters specific to the // currently active bitcoin network. -var activeNetParams = &mainNetParams +var ActiveNetParams = &MainNetParams // params is used to group parameters for various networks such as the main // network and test networks. type params struct { *chaincfg.Params - rpcPort string + RPCPort string } -// mainNetParams contains parameters specific to the main network +// MainNetParams contains parameters specific to the main network // (wire.MainNet). NOTE: The RPC port is intentionally different than the // reference implementation because btcd does not handle wallet requests. The // separate wallet process listens on the well-known port and forwards requests // it does not handle on to btcd. This approach allows the wallet process // to emulate the full reference implementation RPC API. -var mainNetParams = params{ +var MainNetParams = params{ Params: &chaincfg.MainNetParams, - rpcPort: "8334", + RPCPort: "8334", } -// regressionNetParams contains parameters specific to the regression test +// RegressionNetParams contains parameters specific to the regression test // network (wire.TestNet). NOTE: The RPC port is intentionally different // than the reference implementation - see the mainNetParams comment for // details. -var regressionNetParams = params{ +var RegressionNetParams = params{ Params: &chaincfg.RegressionNetParams, - rpcPort: "18334", + RPCPort: "18334", } -// testNet3Params contains parameters specific to the test network (version 3) +// TestNet3Params contains parameters specific to the test network (version 3) // (wire.TestNet3). NOTE: The RPC port is intentionally different than the // reference implementation - see the mainNetParams comment for details. -var testNet3Params = params{ +var TestNet3Params = params{ Params: &chaincfg.TestNet3Params, - rpcPort: "18334", + RPCPort: "18334", } -// simNetParams contains parameters specific to the simulation test network +// SimNetParams contains parameters specific to the simulation test network // (wire.SimNet). -var simNetParams = params{ +var SimNetParams = params{ Params: &chaincfg.SimNetParams, - rpcPort: "18556", + RPCPort: "18556", } -// sigNetParams contains parameters specific to the Signet network +// SigNetParams contains parameters specific to the Signet network // (wire.SigNet). -var sigNetParams = params{ +var SigNetParams = params{ Params: &chaincfg.SigNetParams, - rpcPort: "38332", + RPCPort: "38332", } -// netName returns the name used when referring to a bitcoin network. At the +// NetName returns the name used when referring to a bitcoin network. At the // time of writing, btcd currently places blocks for testnet version 3 in the // data and log directory "testnet", which does not match the Name field of the // chaincfg parameters. This function can be used to override this directory @@ -71,7 +71,7 @@ var sigNetParams = params{ // A proper upgrade to move the data and log directories for this network to // "testnet3" is planned for the future, at which point this function can be // removed and the network parameter's name used instead. -func netName(chainParams *params) string { +func NetName(chainParams *params) string { switch chainParams.Net { case wire.TestNet3: return "testnet" diff --git a/rpcserver.go b/rpcserver.go index 8c94ba21d5..4a474864a8 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -35,6 +35,8 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/database" + "github.com/btcsuite/btcd/internal/config" + "github.com/btcsuite/btcd/internal/log" "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/mining" "github.com/btcsuite/btcd/mining/cpuminer" @@ -378,7 +380,7 @@ func handleAskWallet(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) ( func handleAddNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.AddNodeCmd) - addr := normalizeAddress(c.Addr, s.cfg.ChainParams.DefaultPort) + addr := config.NormalizeAddress(c.Addr, s.cfg.ChainParams.DefaultPort) var err error switch c.SubCmd { case "add": @@ -422,7 +424,7 @@ func handleNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (inter err = s.cfg.ConnMgr.DisconnectByID(int32(nodeID)) } else { if _, _, errP := net.SplitHostPort(c.Target); errP == nil || net.ParseIP(c.Target) != nil { - addr = normalizeAddress(c.Target, params.DefaultPort) + addr = config.NormalizeAddress(c.Target, params.DefaultPort) err = s.cfg.ConnMgr.DisconnectByAddr(addr) } else { return nil, &btcjson.RPCError{ @@ -447,7 +449,7 @@ func handleNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (inter err = s.cfg.ConnMgr.RemoveByID(int32(nodeID)) } else { if _, _, errP := net.SplitHostPort(c.Target); errP == nil || net.ParseIP(c.Target) != nil { - addr = normalizeAddress(c.Target, params.DefaultPort) + addr = config.NormalizeAddress(c.Target, params.DefaultPort) err = s.cfg.ConnMgr.RemoveByAddr(addr) } else { return nil, &btcjson.RPCError{ @@ -464,7 +466,7 @@ func handleNode(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (inter } case "connect": - addr = normalizeAddress(c.Target, params.DefaultPort) + addr = config.NormalizeAddress(c.Target, params.DefaultPort) // Default to temporary connections. subCmd := "temp" @@ -635,10 +637,10 @@ func handleDebugLevel(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) // Special show command to list supported subsystems. if c.LevelSpec == "show" { return fmt.Sprintf("Supported subsystems %v", - supportedSubsystems()), nil + config.SupportedSubsystems()), nil } - err := parseAndSetDebugLevels(c.LevelSpec) + err := config.ParseAndSetDebugLevels(c.LevelSpec) if err != nil { return nil, &btcjson.RPCError{ Code: btcjson.ErrRPCInvalidParams.Code, @@ -892,7 +894,7 @@ func handleEstimateFee(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) func generate(s *rpcServer, blocks uint32) ([]string, error) { // Respond with an error if there are no addresses to pay the // created blocks to. - if len(cfg.miningAddrs) == 0 { + if len(cfg.MiningAddrs) == 0 { return nil, &btcjson.RPCError{ Code: btcjson.ErrRPCInternal.Code, Message: "No payment addresses specified " + @@ -1026,7 +1028,7 @@ func handleGetAddedNodeInfo(s *rpcServer, cmd interface{}, closeChan <-chan stru default: // Do a DNS lookup for the address. If the lookup fails, just // use the host. - ips, err := btcdLookup(host) + ips, err := cfg.Lookup(host) if err != nil { ipList = make([]string, 1) ipList[0] = host @@ -1045,7 +1047,7 @@ func handleGetAddedNodeInfo(s *rpcServer, cmd interface{}, closeChan <-chan stru addr.Address = ip addr.Connected = "false" if ip == host && peer.Connected() { - addr.Connected = directionString(peer.Inbound()) + addr.Connected = log.DirectionString(peer.Inbound()) } addrs = append(addrs, addr) } @@ -1611,7 +1613,7 @@ func (state *gbtWorkState) updateBlockTemplate(s *rpcServer, useCoinbaseValue bo // to create their own coinbase. var payAddr btcutil.Address if !useCoinbaseValue { - payAddr = cfg.miningAddrs[rand.Intn(len(cfg.miningAddrs))] + payAddr = cfg.MiningAddrs[rand.Intn(len(cfg.MiningAddrs))] } // Create a new block template that has a coinbase which anyone @@ -1666,7 +1668,7 @@ func (state *gbtWorkState) updateBlockTemplate(s *rpcServer, useCoinbaseValue bo // returned if none have been specified. if !useCoinbaseValue && !template.ValidPayAddress { // Choose a payment address at random. - payToAddr := cfg.miningAddrs[rand.Intn(len(cfg.miningAddrs))] + payToAddr := cfg.MiningAddrs[rand.Intn(len(cfg.MiningAddrs))] // Update the block coinbase output of the template to // pay to the randomly selected payment address. @@ -1975,7 +1977,7 @@ func handleGetBlockTemplateRequest(s *rpcServer, request *btcjson.TemplateReques // When a coinbase transaction has been requested, respond with an error // if there are no addresses to pay the created block template to. - if !useCoinbaseValue && len(cfg.miningAddrs) == 0 { + if !useCoinbaseValue && len(cfg.MiningAddrs) == 0 { return nil, &btcjson.RPCError{ Code: btcjson.ErrRPCInternal.Code, Message: "A coinbase transaction has been requested, " + @@ -2383,7 +2385,7 @@ func handleGetInfo(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (in Proxy: cfg.Proxy, Difficulty: getDifficultyRatio(best.Bits, s.cfg.ChainParams), TestNet: cfg.TestNet3, - RelayFee: cfg.minRelayTxFee.ToBTC(), + RelayFee: cfg.MinRelayTxFee.ToBTC(), } return ret, nil @@ -3569,7 +3571,7 @@ func handleSetGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) } else { // Respond with an error if there are no addresses to pay the // created blocks to. - if len(cfg.miningAddrs) == 0 { + if len(cfg.MiningAddrs) == 0 { return nil, &btcjson.RPCError{ Code: btcjson.ErrRPCInternal.Code, Message: "No payment addresses specified " + diff --git a/server.go b/server.go index 66794e4bb7..e45ac30be1 100644 --- a/server.go +++ b/server.go @@ -31,6 +31,9 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/connmgr" "github.com/btcsuite/btcd/database" + "github.com/btcsuite/btcd/internal/config" + "github.com/btcsuite/btcd/internal/log" + "github.com/btcsuite/btcd/internal/params" "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/mining" "github.com/btcsuite/btcd/mining/cpuminer" @@ -360,7 +363,7 @@ func (sp *serverPeer) pushAddrMsg(addresses []*wire.NetAddressV2) { known, err := sp.PushAddrV2Msg(addrs) if err != nil { - peerLog.Errorf("Can't push addrv2 message to %s: %v", + log.PeerLog.Errorf("Can't push addrv2 message to %s: %v", sp.Peer, err) sp.Disconnect() return @@ -1432,14 +1435,14 @@ func (sp *serverPeer) OnNotFound(p *peer.Peer, msg *wire.MsgNotFound) { } } if numBlocks > 0 { - blockStr := pickNoun(uint64(numBlocks), "block", "blocks") + blockStr := log.PickNoun(uint64(numBlocks), "block", "blocks") reason := fmt.Sprintf("%d %v not found", numBlocks, blockStr) if sp.addBanScore(20*numBlocks, 0, reason) { return } } if numTxns > 0 { - txStr := pickNoun(uint64(numTxns), "transaction", "transactions") + txStr := log.PickNoun(uint64(numTxns), "transaction", "transactions") reason := fmt.Sprintf("%d %v not found", numTxns, txStr) if sp.addBanScore(0, 10*numTxns, reason) { return @@ -1859,7 +1862,7 @@ func (s *server) handleBanPeerMsg(state *peerState, sp *serverPeer) { srvrLog.Debugf("can't split ban peer %s %v", sp.Addr(), err) return } - direction := directionString(sp.Inbound()) + direction := log.DirectionString(sp.Inbound()) srvrLog.Infof("Banned peer %s (%s) for %v", host, direction, cfg.BanDuration) state.banned[host] = time.Now().Add(cfg.BanDuration) @@ -2218,7 +2221,7 @@ func (s *server) peerDoneHandler(sp *serverPeer) { numEvicted := s.txMemPool.RemoveOrphansByTag(mempool.Tag(sp.ID())) if numEvicted > 0 { txmpLog.Debugf("Evicted %d %s from peer %v (id %d)", - numEvicted, pickNoun(numEvicted, "orphan", + numEvicted, log.PickNoun(numEvicted, "orphan", "orphans"), sp, sp.ID()) } } @@ -2249,8 +2252,8 @@ func (s *server) peerHandler() { if !cfg.DisableDNSSeed { // Add peers discovered through DNS to the address manager. - connmgr.SeedFromDNS(activeNetParams.Params, defaultRequiredServices, - btcdLookup, func(addrs []*wire.NetAddressV2) { + connmgr.SeedFromDNS(params.ActiveNetParams.Params, defaultRequiredServices, + cfg.Lookup, func(addrs []*wire.NetAddressV2) { // Bitcoind uses a lookup of the dns seeder here. This // is rather strange since the values looked up by the // DNS seed lookups will vary quite a lot. @@ -2618,7 +2621,7 @@ func (s *server) upnpUpdateThread() { // Go off immediately to prevent code duplication, thereafter we renew // lease every 15 minutes. timer := time.NewTimer(0 * time.Second) - lport, _ := strconv.ParseInt(activeNetParams.DefaultPort, 10, 16) + lport, _ := strconv.ParseInt(params.ActiveNetParams.DefaultPort, 10, 16) first := true out: for { @@ -2735,7 +2738,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, services &^= wire.SFNodeNetwork } - amgr := addrmgr.New(cfg.DataDir, btcdLookup) + amgr := addrmgr.New(cfg.DataDir, cfg.Lookup) var listeners []net.Listener var nat NAT @@ -2821,7 +2824,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, // Merge given checkpoints with the default ones unless they are disabled. var checkpoints []chaincfg.Checkpoint if !cfg.DisableCheckpoints { - checkpoints = mergeCheckpoints(s.chainParams.Checkpoints, cfg.addCheckpoints) + checkpoints = mergeCheckpoints(s.chainParams.Checkpoints, cfg.AddCheckpoints) } // Log that the node is pruned. @@ -2883,9 +2886,9 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, AcceptNonStd: cfg.RelayNonStd, FreeTxRelayLimit: cfg.FreeTxRelayLimit, MaxOrphanTxs: cfg.MaxOrphanTxs, - MaxOrphanTxSize: defaultMaxOrphanTxSize, + MaxOrphanTxSize: config.DefaultMaxOrphanTxSize, MaxSigOpCostPerTx: blockchain.MaxBlockSigOpsCost / 4, - MinRelayTxFee: cfg.minRelayTxFee, + MinRelayTxFee: cfg.MinRelayTxFee, MaxTxVersion: 2, RejectReplacement: cfg.RejectReplacement, }, @@ -2928,7 +2931,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, BlockMinSize: cfg.BlockMinSize, BlockMaxSize: cfg.BlockMaxSize, BlockPrioritySize: cfg.BlockPrioritySize, - TxMinFreeFee: cfg.minRelayTxFee, + TxMinFreeFee: cfg.MinRelayTxFee, } blockTemplateGenerator := mining.NewBlkTmplGenerator(&policy, s.chainParams, s.txMemPool, s.chain, s.timeSource, @@ -2936,7 +2939,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, s.cpuMiner = cpuminer.New(&cpuminer.Config{ ChainParams: chainParams, BlockTemplateGenerator: blockTemplateGenerator, - MiningAddrs: cfg.miningAddrs, + MiningAddrs: cfg.MiningAddrs, ProcessBlock: s.syncManager.ProcessBlock, ConnectedCount: s.ConnectedCount, IsCurrent: s.syncManager.IsCurrent, @@ -2976,7 +2979,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, // allow nondefault ports after 50 failed tries. if tries < 50 && fmt.Sprintf("%d", addr.NetAddress().Port) != - activeNetParams.DefaultPort { + params.ActiveNetParams.DefaultPort { continue } @@ -3001,7 +3004,7 @@ func newServer(listenAddrs, agentBlacklist, agentWhitelist []string, OnAccept: s.inboundPeerConnected, RetryDuration: connectionRetryInterval, TargetOutbound: uint32(targetOutbound), - Dial: btcdDial, + Dial: cfg.Dial, OnConnection: s.outboundPeerConnected, GetNewAddress: newAddressFunc, }) @@ -3091,10 +3094,10 @@ func initListeners(amgr *addrmgr.AddrManager, listenAddrs []string, services wir var nat NAT if len(cfg.ExternalIPs) != 0 { - defaultPort, err := strconv.ParseUint(activeNetParams.DefaultPort, 10, 16) + defaultPort, err := strconv.ParseUint(params.ActiveNetParams.DefaultPort, 10, 16) if err != nil { srvrLog.Errorf("Can not parse default port %s for active chain: %v", - activeNetParams.DefaultPort, err) + params.ActiveNetParams.DefaultPort, err) return nil, nil, err } @@ -3181,7 +3184,7 @@ func addrStringToNetAddr(addr string) (net.Addr, error) { } // Attempt to look up an IP address associated with the parsed host. - ips, err := btcdLookup(host) + ips, err := cfg.Lookup(host) if err != nil { return nil, err } @@ -3268,7 +3271,7 @@ func dynamicTickDuration(remaining time.Duration) time.Duration { // isWhitelisted returns whether the IP address is included in the whitelisted // networks and IPs. func isWhitelisted(addr net.Addr) bool { - if len(cfg.whitelists) == 0 { + if len(cfg.Whitelists) == 0 { return false } @@ -3283,7 +3286,7 @@ func isWhitelisted(addr net.Addr) bool { return false } - for _, ipnet := range cfg.whitelists { + for _, ipnet := range cfg.Whitelists { if ipnet.Contains(ip) { return true } diff --git a/upgrade.go b/upgrade.go index 5dec8d4c6a..bd817ef981 100644 --- a/upgrade.go +++ b/upgrade.go @@ -8,6 +8,8 @@ import ( "io" "os" "path/filepath" + + "github.com/btcsuite/btcd/internal/config" ) // dirEmpty returns whether or not the specified directory path is empty. @@ -109,7 +111,7 @@ func upgradeDBPaths() error { func upgradeDataPaths() error { // No need to migrate if the old and new home paths are the same. oldHomePath := oldBtcdHomeDir() - newHomePath := defaultHomeDir + newHomePath := config.DefaultHomeDir if oldHomePath == newHomePath { return nil } @@ -125,8 +127,8 @@ func upgradeDataPaths() error { } // Move old btcd.conf into new location if needed. - oldConfPath := filepath.Join(oldHomePath, defaultConfigFilename) - newConfPath := filepath.Join(newHomePath, defaultConfigFilename) + oldConfPath := filepath.Join(oldHomePath, config.DefaultConfigFilename) + newConfPath := filepath.Join(newHomePath, config.DefaultConfigFilename) if fileExists(oldConfPath) && !fileExists(newConfPath) { err := os.Rename(oldConfPath, newConfPath) if err != nil { @@ -135,8 +137,8 @@ func upgradeDataPaths() error { } // Move old data directory into new location if needed. - oldDataPath := filepath.Join(oldHomePath, defaultDataDirname) - newDataPath := filepath.Join(newHomePath, defaultDataDirname) + oldDataPath := filepath.Join(oldHomePath, config.DefaultDataDirname) + newDataPath := filepath.Join(newHomePath, config.DefaultDataDirname) if fileExists(oldDataPath) && !fileExists(newDataPath) { err := os.Rename(oldDataPath, newDataPath) if err != nil { From 70bbc17ccc4c33ede62d85bdc497e231ff234537 Mon Sep 17 00:00:00 2001 From: Linden <70739041+linden@users.noreply.github.com> Date: Sun, 29 Sep 2024 12:25:59 -0700 Subject: [PATCH 3/9] feat: add `btcdctrl` `btcdctrl` makes it easy to programmatically run `btcd` - with a defined configuration and sensible defaults, it eases testing and managing `btcd` in production. --- btcdctrl/btcdctrl.go | 358 ++++++++++++++++++++++++++++++++++++++ btcdctrl/btcdctrl_test.go | 60 +++++++ internal/config/config.go | 8 +- 3 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 btcdctrl/btcdctrl.go create mode 100644 btcdctrl/btcdctrl_test.go diff --git a/btcdctrl/btcdctrl.go b/btcdctrl/btcdctrl.go new file mode 100644 index 0000000000..6787e90533 --- /dev/null +++ b/btcdctrl/btcdctrl.go @@ -0,0 +1,358 @@ +package btcdctrl + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "os/exec" + "reflect" + "strconv" + "strings" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/internal/config" + "github.com/btcsuite/btcd/rpcclient" +) + +type Config = config.Config + +func NewDefaultConfig() *Config { + cfg := config.NewDefaultConfig() + + // Remove the configuration file, as everything is supplied in flags. + cfg.ConfigFile = "" + + return &cfg +} + +// Create a new random address. +func newRandAddress(chain *chaincfg.Params) (btcutil.Address, error) { + // Generate a new private key. + prv, err := btcec.NewPrivateKey() + if err != nil { + return nil, err + } + + // Derive a public key, then serialize it. + pk := prv.PubKey().SerializeUncompressed() + + // Create a new pay-to-pubkey address. + return btcutil.NewAddressPubKey(pk, chain) +} + +func NewTestConfig(tmp string) (*Config, error) { + cfg := NewDefaultConfig() + + // Set the network to simnet. + cfg.SimNet = true + + // Enable generating blocks. + cfg.Generate = true + + // Create a new random address. + addr, err := newRandAddress(&chaincfg.SimNetParams) + if err != nil { + return nil, err + } + + // Use the random address as the mining address, required for generate. + cfg.MiningAddrs = []btcutil.Address{addr} + + // Set the default credentials to user:pass. + cfg.RPCUser = "user" + cfg.RPCPass = "pass" + + // Change the root directory to the temporary directory. + cfg.ChangeRoot(tmp) + + // Listen on 127.0.0.1 on an OS assigned port. + cfg.RPCListeners = []string{"127.0.0.1:0"} + cfg.Listeners = []string{"127.0.0.1:0"} + + return cfg, nil +} + +type ControllerConfig struct { + Stderr io.Writer + Stdout io.Writer + + *Config +} + +type Controller struct { + *rpcclient.Client + + cmd *exec.Cmd + + rpc string + p2p string + + cfg *ControllerConfig +} + +// gracefully shutdown btcd instance using SIGINT and wait for it to exit. +func (c *Controller) Stop() error { + // Signal SIGINT, for graceful shutdown. + err := c.cmd.Process.Signal(os.Interrupt) + if err != nil { + return err + } + + // Wait for the program to exit. + for { + exit, err := c.cmd.Process.Wait() + if err != nil { + return err + } + + // Check if the program exited. + if exit.Exited() { + break + } + } + + return nil +} + +func (c *Controller) RPCAddress() string { + return c.rpc +} + +func (c *Controller) P2PAddress() string { + return c.p2p +} + +func (c *Controller) RPCConnConfig() (*rpcclient.ConnConfig, error) { + cert, err := os.ReadFile(c.cfg.Config.RPCCert) + if err != nil { + return nil, err + } + + return &rpcclient.ConnConfig{ + Host: c.rpc, + + User: c.cfg.RPCUser, + Pass: c.cfg.RPCPass, + + Endpoint: "ws", + + Certificates: cert, + }, nil +} + +// Start btcd and wait for the RPC to be ready. +func (c *Controller) Start() error { + var args []string + + // First, we convert the `Config` type into flags. + + // struct value. + sv := reflect.ValueOf(*c.cfg.Config) + + // Iterate over visible fields. + for i, ft := range reflect.VisibleFields(sv.Type()) { + // Skip unexported fields, unused by the flag parser. + if !ft.IsExported() { + continue + } + + // Check for a long tag, we'll use this over the short tag to ease debugging. + name := ft.Tag.Get("long") + + // Skip any fields without a tag, likely unused by the flag parser. + if name == "" { + continue + } + + // Get the field value. + fv := sv.Field(i) + + var val string + + // Encode the field value to a string. + switch iface := fv.Interface(); field := iface.(type) { + case time.Duration: + // Encode the duration into milliseconds. + val = fmt.Sprintf("%dms", field.Milliseconds()) + + case string: + // Skip empty fields, indicating they're unused. + if field == "" { + continue + } + + // Quote the string to avoid command injection. + val = strconv.Quote(field) + + case []string: + // Skip empty slices, indicates unused. + if len(field) == 0 { + continue + } + + // Handle listeners differently, each of them need their own flag. + if name == "listen" || name == "rpclisten" { + for _, f := range field { + args = append(args, fmt.Sprintf("--%s", name), strconv.Quote(f)) + } + + continue + } + + // Quote the string to avoid command injection. + val = strconv.Quote(strings.Join(field, ",")) + + case bool: + // Lack of the flag means false, so skip. + if !field { + continue + } + + // Encode all numbers via `fmt`, handles edge cases for us. + case float64: + val = fmt.Sprintf("%f", fv.Float()) + + case uint, uint16, uint32, uint64: + val = fmt.Sprintf("%d", fv.Uint()) + + case int, int16, int32, int64, btcutil.Amount: + val = fmt.Sprintf("%d", fv.Int()) + + // Technically a valid value for slices, should be skipped. + case nil: + continue + + default: + return errors.New("unknown type") + } + + // Append the flag name. + args = append(args, fmt.Sprintf("--%s", name)) + + // Append the flag value if found (skipped on bools). + if val != "" { + args = append(args, val) + } + } + + // Handle fields that depend on custom encoding/decoding. + + // Encode checkpoints. + for _, chk := range c.cfg.Config.AddCheckpoints { + args = append(args, "--addcheckpoint", fmt.Sprintf("%d:%s", chk.Height, chk.Hash)) + } + + // Encode mining addresses. + for _, addr := range c.cfg.Config.MiningAddrs { + args = append(args, "--miningaddr", addr.EncodeAddress()) + } + + // Encode whitelists. + for _, addr := range c.cfg.Config.Whitelists { + args = append(args, "--whitelist", addr.String()) + } + + // Create the command. + c.cmd = exec.Command("btcd", args...) + + // Create a pipe of stdout. + pr, pw, err := os.Pipe() + if err != nil { + return err + } + + // Match the output configuration. + c.cmd.Stderr = c.cfg.Stderr + + if c.cmd.Stdout != nil { + c.cmd.Stdout = io.MultiWriter(c.cfg.Stdout, pw) + } else { + c.cmd.Stdout = pw + } + + // Execute the command. + err = c.cmd.Start() + if err != nil { + return err + } + + // Scan the stdout line by line. + scan := bufio.NewScanner(pr) + + rpc := c.cfg.DisableRPC + p2p := false + + // Scan each line until both RPC (if enabled) and P2P addresses are found. + for scan.Scan() && (!rpc || !p2p) { + line := scan.Text() + + _, addr, ok := strings.Cut(line, "RPC server listening on ") + if ok { + c.rpc = addr + rpc = true + continue + } + + _, addr, ok = strings.Cut(line, "Server listening on ") + if !ok { + c.p2p = addr + p2p = true + continue + } + } + + // Return early if RPC is disabled. + if !rpc { + return nil + } + + deadline := time.Now().Add(30 * time.Second) + + // Try to connect via RPC for 30 seconds. + for deadline.After(time.Now()) { + var cfg *rpcclient.ConnConfig + // Create the RPC config. + cfg, err = c.RPCConnConfig() + if err != nil { + continue + } + + // Create the RPC client. + c.Client, err = rpcclient.New(cfg, nil) + if err != nil { + continue + } + + // Ping the RPC client. + err = c.Client.Ping() + if err != nil { + continue + } + + err = nil + break + } + + // Check if the client was created. + if c.Client == nil { + // Check if the connection loop exited with an error. + if err != nil { + return errors.Join(errors.New("timeout"), err) + } + + return errors.New("timeout") + } + + return nil +} + +func New(cfg *ControllerConfig) *Controller { + return &Controller{ + cfg: cfg, + } +} diff --git a/btcdctrl/btcdctrl_test.go b/btcdctrl/btcdctrl_test.go new file mode 100644 index 0000000000..8814619348 --- /dev/null +++ b/btcdctrl/btcdctrl_test.go @@ -0,0 +1,60 @@ +package btcdctrl_test + +import ( + "fmt" + "os" + + "github.com/btcsuite/btcd/btcdctrl" +) + +func ExampleController() { + // Create a temporary directory for the wallet data. + tmp, err := os.MkdirTemp("", "") + if err != nil { + panic(err) + } + + // Create a new test-oriented configration. + cfg, err := btcdctrl.NewTestConfig(tmp) + if err != nil { + panic(err) + } + + // Create a new controller. + c := btcdctrl.New(&btcdctrl.ControllerConfig{ + Stderr: os.Stderr, + Stdout: os.Stdout, + + Config: cfg, + }) + + // Start btcd. + err = c.Start() + if err != nil { + panic(err) + } + + // Stop btcd on exit. + defer c.Stop() + + // Enable generation. + err = c.SetGenerate(true, 0) + if err != nil { + panic(err) + } + + // Generate 100 blocks. + _, err = c.Generate(100) + if err != nil { + panic(err) + } + + // Query info. + info, err := c.GetInfo() + if err != nil { + panic(err) + } + + fmt.Println(info.Blocks) + // Output: 100 +} diff --git a/internal/config/config.go b/internal/config/config.go index ccea1b98fe..a3e1ad5d7e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -66,7 +66,7 @@ const ( DefaultMaxOrphanTxSize = 100000 DefaultSigCacheMaxSize = 100000 DefaultUtxoCacheMaxSizeMiB = 250 - SampleConfigFilename = "sample-btcd.conf" + SampleConfigFilename = "../../sample-btcd.conf" DefaultTxIndex = false DefaultAddrIndex = false PruneMinSize = 1536 @@ -189,7 +189,11 @@ func (cfg *Config) ChangeRoot(root string) { cfg.RPCKey = filepath.Join(root, "rpc.key") cfg.DataDir = filepath.Join(root, "data") cfg.RPCCert = filepath.Join(root, "rpc.cert") - cfg.ConfigFile = filepath.Join(root, "btcd.conf") + + // Only change the configuration file if supplied with one. + if cfg.ConfigFile != "" { + cfg.ConfigFile = filepath.Join(root, "btcd.conf") + } } // Dial connects to the address on the named network using the appropriate From 70fb0df96572626328372e49206f451aeacf7d41 Mon Sep 17 00:00:00 2001 From: Linden <70739041+linden@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:02:51 -0700 Subject: [PATCH 4/9] fix(`btcdctrl`): set `stdout` correctly --- btcdctrl/btcdctrl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/btcdctrl/btcdctrl.go b/btcdctrl/btcdctrl.go index 6787e90533..ac8dd330cf 100644 --- a/btcdctrl/btcdctrl.go +++ b/btcdctrl/btcdctrl.go @@ -269,7 +269,7 @@ func (c *Controller) Start() error { // Match the output configuration. c.cmd.Stderr = c.cfg.Stderr - if c.cmd.Stdout != nil { + if c.cfg.Stdout != nil { c.cmd.Stdout = io.MultiWriter(c.cfg.Stdout, pw) } else { c.cmd.Stdout = pw From 069afc4d1767d281a9c47e24e7db9c7249d5e163 Mon Sep 17 00:00:00 2001 From: Linden <70739041+linden@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:06:18 -0700 Subject: [PATCH 5/9] fix(`btcdctrl`): handle & test for flaky edge-cases 1. killing a process then waiting for it can return an error depending on state, instead just exit the loop. 2. websockets hang indefinitely every 20 tests or so, use HTTP POST instead. 3. handle the RPC/P2P message being the last line in the log. --- btcdctrl/btcdctrl.go | 15 ++++--- btcdctrl/btcdctrl_test.go | 83 ++++++++++++++------------------------- btcdctrl/example_test.go | 57 +++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 59 deletions(-) create mode 100644 btcdctrl/example_test.go diff --git a/btcdctrl/btcdctrl.go b/btcdctrl/btcdctrl.go index ac8dd330cf..b972037f09 100644 --- a/btcdctrl/btcdctrl.go +++ b/btcdctrl/btcdctrl.go @@ -107,7 +107,7 @@ func (c *Controller) Stop() error { for { exit, err := c.cmd.Process.Wait() if err != nil { - return err + break } // Check if the program exited. @@ -139,9 +139,9 @@ func (c *Controller) RPCConnConfig() (*rpcclient.ConnConfig, error) { User: c.cfg.RPCUser, Pass: c.cfg.RPCPass, - Endpoint: "ws", - Certificates: cert, + + HTTPPostMode: true, }, nil } @@ -288,21 +288,24 @@ func (c *Controller) Start() error { p2p := false // Scan each line until both RPC (if enabled) and P2P addresses are found. - for scan.Scan() && (!rpc || !p2p) { + for !rpc || !p2p { line := scan.Text() _, addr, ok := strings.Cut(line, "RPC server listening on ") if ok { c.rpc = addr rpc = true - continue } _, addr, ok = strings.Cut(line, "Server listening on ") if !ok { c.p2p = addr p2p = true - continue + } + + // Ensure we've not found the RPC and P2P addresses, so we can continue scanning. + if (!rpc || !p2p) && !scan.Scan() { + break } } diff --git a/btcdctrl/btcdctrl_test.go b/btcdctrl/btcdctrl_test.go index 8814619348..b1431c2132 100644 --- a/btcdctrl/btcdctrl_test.go +++ b/btcdctrl/btcdctrl_test.go @@ -1,60 +1,37 @@ -package btcdctrl_test +package btcdctrl import ( - "fmt" "os" - - "github.com/btcsuite/btcd/btcdctrl" + "testing" ) -func ExampleController() { - // Create a temporary directory for the wallet data. - tmp, err := os.MkdirTemp("", "") - if err != nil { - panic(err) - } - - // Create a new test-oriented configration. - cfg, err := btcdctrl.NewTestConfig(tmp) - if err != nil { - panic(err) - } - - // Create a new controller. - c := btcdctrl.New(&btcdctrl.ControllerConfig{ - Stderr: os.Stderr, - Stdout: os.Stdout, - - Config: cfg, - }) - - // Start btcd. - err = c.Start() - if err != nil { - panic(err) +func TestFail(t *testing.T) { + for i := 0; i < 100; i++ { + cfg, err := NewTestConfig(t.TempDir()) + if err != nil { + t.Fatal(err) + } + + cfg.DebugLevel = "trace" + + // Create a new controller. + c := New(&ControllerConfig{ + Stderr: os.Stderr, + Stdout: os.Stdout, + + Config: cfg, + }) + + // Start btcd. + err = c.Start() + if err != nil { + t.Fatal(err) + } + + // Stop btcd. + err = c.Stop() + if err != nil { + t.Fatal(err) + } } - - // Stop btcd on exit. - defer c.Stop() - - // Enable generation. - err = c.SetGenerate(true, 0) - if err != nil { - panic(err) - } - - // Generate 100 blocks. - _, err = c.Generate(100) - if err != nil { - panic(err) - } - - // Query info. - info, err := c.GetInfo() - if err != nil { - panic(err) - } - - fmt.Println(info.Blocks) - // Output: 100 } diff --git a/btcdctrl/example_test.go b/btcdctrl/example_test.go new file mode 100644 index 0000000000..cbc21291c7 --- /dev/null +++ b/btcdctrl/example_test.go @@ -0,0 +1,57 @@ +package btcdctrl_test + +import ( + "fmt" + "os" + + "github.com/btcsuite/btcd/btcdctrl" +) + +func ExampleController() { + // Create a temporary directory for the wallet data. + tmp, err := os.MkdirTemp("", "") + if err != nil { + panic(err) + } + + // Create a new test-oriented configration. + cfg, err := btcdctrl.NewTestConfig(tmp) + if err != nil { + panic(err) + } + + // Create a new controller. + c := btcdctrl.New(&btcdctrl.ControllerConfig{ + Config: cfg, + }) + + // Start btcd. + err = c.Start() + if err != nil { + panic(err) + } + + // Stop btcd on exit. + defer c.Stop() + + // Enable generation. + err = c.SetGenerate(true, 0) + if err != nil { + panic(err) + } + + // Generate 100 blocks. + _, err = c.Generate(100) + if err != nil { + panic(err) + } + + // Query info. + info, err := c.GetInfo() + if err != nil { + panic(err) + } + + fmt.Println(info.Blocks) + // Output: 100 +} From 4877c3840c5e534579fa0ea476ca7a2e75db9549 Mon Sep 17 00:00:00 2001 From: Linden <70739041+linden@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:53:14 -0700 Subject: [PATCH 6/9] fix(`btcdctrl`): read the output pipe continuously was missed by tests as this only appears on long-lived processes, eventually the pipe will run out of space causing `btcd` to hang. --- btcdctrl/btcdctrl.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/btcdctrl/btcdctrl.go b/btcdctrl/btcdctrl.go index b972037f09..fa4345ee4e 100644 --- a/btcdctrl/btcdctrl.go +++ b/btcdctrl/btcdctrl.go @@ -268,12 +268,7 @@ func (c *Controller) Start() error { // Match the output configuration. c.cmd.Stderr = c.cfg.Stderr - - if c.cfg.Stdout != nil { - c.cmd.Stdout = io.MultiWriter(c.cfg.Stdout, pw) - } else { - c.cmd.Stdout = pw - } + c.cmd.Stdout = pw // Execute the command. err = c.cmd.Start() @@ -281,8 +276,15 @@ func (c *Controller) Start() error { return err } + var r io.Reader = pr + + // Pipe the early output to stdout if configured. + if c.cfg.Stdout != nil { + r = io.TeeReader(pr, c.cfg.Stdout) + } + // Scan the stdout line by line. - scan := bufio.NewScanner(pr) + scan := bufio.NewScanner(r) rpc := c.cfg.DisableRPC p2p := false @@ -314,6 +316,17 @@ func (c *Controller) Start() error { return nil } + // Discard as a fallback. + stdout := io.Discard + + // Use the configured stdout by default. + if c.cfg.Stdout != nil { + stdout = c.cfg.Stdout + } + + // The pipe needs to continuously be read, otherwise `btcd` will hang. + go io.Copy(stdout, pr) + deadline := time.Now().Add(30 * time.Second) // Try to connect via RPC for 30 seconds. From 0fcdd9fefd28fd25b01f242ec1907cf75b960d1b Mon Sep 17 00:00:00 2001 From: Linden <70739041+linden@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:54:11 -0700 Subject: [PATCH 7/9] fix(`btcdctrl`): handle timeout cases separately the client could exist but with an error case, this should be handled. --- btcdctrl/btcdctrl.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/btcdctrl/btcdctrl.go b/btcdctrl/btcdctrl.go index fa4345ee4e..77b4fb6ccf 100644 --- a/btcdctrl/btcdctrl.go +++ b/btcdctrl/btcdctrl.go @@ -354,13 +354,13 @@ func (c *Controller) Start() error { break } + // Check if the connection loop exited with an error. + if err != nil { + return errors.Join(errors.New("timeout"), err) + } + // Check if the client was created. if c.Client == nil { - // Check if the connection loop exited with an error. - if err != nil { - return errors.Join(errors.New("timeout"), err) - } - return errors.New("timeout") } From 1c6f7de57f6fa3a06d1bec2135092f18dbd9517c Mon Sep 17 00:00:00 2001 From: Linden <70739041+linden@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:48:34 -0800 Subject: [PATCH 8/9] fix: set chain params when parsing address (`generatetoaddress`) --- rpcserver.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rpcserver.go b/rpcserver.go index 4a474864a8..34c743ac46 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -37,6 +37,7 @@ import ( "github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/internal/config" "github.com/btcsuite/btcd/internal/log" + "github.com/btcsuite/btcd/internal/params" "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/mining" "github.com/btcsuite/btcd/mining/cpuminer" @@ -945,7 +946,7 @@ func generate(s *rpcServer, blocks uint32) ([]string, error) { func handleGenerateToAddress(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { c := cmd.(*btcjson.GenerateToAddressCmd) - addr, err := btcutil.DecodeAddress(c.Address, nil) + addr, err := btcutil.DecodeAddress(c.Address, params.ActiveNetParams.Params) if err != nil { return nil, err } From 598a9818bbbe521b527f1728fa91a1cc713f587d Mon Sep 17 00:00:00 2001 From: Linden <70739041+linden@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:12:40 -0800 Subject: [PATCH 9/9] feat(`btcdctrl`): allow specifying `btcd` path --- btcdctrl/btcdctrl.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/btcdctrl/btcdctrl.go b/btcdctrl/btcdctrl.go index 77b4fb6ccf..fde3abe849 100644 --- a/btcdctrl/btcdctrl.go +++ b/btcdctrl/btcdctrl.go @@ -81,6 +81,8 @@ type ControllerConfig struct { Stderr io.Writer Stdout io.Writer + Path string + *Config } @@ -257,8 +259,13 @@ func (c *Controller) Start() error { args = append(args, "--whitelist", addr.String()) } + name := "btcd" + if c.cfg.Path != "" { + name = c.cfg.Path + } + // Create the command. - c.cmd = exec.Command("btcd", args...) + c.cmd = exec.Command(name, args...) // Create a pipe of stdout. pr, pw, err := os.Pipe()