diff --git a/background/background.go b/background/background.go index de38780..0f1fc2c 100644 --- a/background/background.go +++ b/background/background.go @@ -12,12 +12,12 @@ import ( ) type NotificationHandler struct { - Ctx context.Context - Db *database.VspDatabase - WalletConnect []rpc.Connect - NetParams *chaincfg.Params - closed chan struct{} - dcrdClient *rpc.DcrdRPC + Ctx context.Context + Db *database.VspDatabase + Wallets rpc.WalletConnect + NetParams *chaincfg.Params + closed chan struct{} + dcrdClient *rpc.DcrdRPC } const ( @@ -108,20 +108,11 @@ func (n *NotificationHandler) Notify(method string, params json.RawMessage) erro return nil } - walletClients := make([]*rpc.WalletRPC, len(n.WalletConnect)) - for i := 0; i < len(n.WalletConnect); i++ { - walletConn, err := n.WalletConnect[i]() - if err != nil { - log.Errorf("dcrwallet connection error: %v", err) - // If this fails, there is nothing more we can do. Return. - return nil - } - walletClients[i], err = rpc.WalletClient(n.Ctx, walletConn, n.NetParams) - if err != nil { - log.Errorf("dcrwallet client error: %v", err) - // If this fails, there is nothing more we can do. Return. - return nil - } + walletClients, err := n.Wallets.Clients(n.Ctx, n.NetParams) + if err != nil { + log.Error(err) + // If this fails, there is nothing more we can do. Return. + return nil } for _, ticket := range unconfirmedFees { @@ -186,12 +177,9 @@ func (n *NotificationHandler) Close() error { return nil } -func (n *NotificationHandler) connect(dcrdConnect rpc.Connect) error { - dcrdConn, err := dcrdConnect() - if err != nil { - return err - } - n.dcrdClient, err = rpc.DcrdClient(n.Ctx, dcrdConn, n.NetParams) +func (n *NotificationHandler) connect(dcrdConnect rpc.DcrdConnect) error { + var err error + n.dcrdClient, err = dcrdConnect.Client(n.Ctx, n.NetParams) if err != nil { return err } @@ -213,7 +201,7 @@ func (n *NotificationHandler) connect(dcrdConnect rpc.Connect) error { } } -func Start(n *NotificationHandler, dcrdConnect rpc.Connect) { +func Start(n *NotificationHandler, dcrdConnect rpc.DcrdConnect) { // Loop forever attempting to create a connection to the dcrd server. go func() { diff --git a/main.go b/main.go index 3fef7a9..81e2585 100644 --- a/main.go +++ b/main.go @@ -64,56 +64,38 @@ func run(ctx context.Context) error { // Create RPC client for local dcrd instance (used for broadcasting and // checking the status of fee transactions). - // Dial once just to validate config. - dcrdConnect := rpc.Setup(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass, + dcrd := rpc.SetupDcrd(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert, nil) - dcrdConn, err := dcrdConnect() + // Dial once just to validate config. + _, err = dcrd.Client(ctx, cfg.netParams.Params) if err != nil { - log.Errorf("dcrd connection error: %v", err) - requestShutdown() - shutdownWg.Wait() - return err - } - _, err = rpc.DcrdClient(ctx, dcrdConn, cfg.netParams.Params) - if err != nil { - log.Errorf("dcrd client error: %v", err) + log.Error(err) requestShutdown() shutdownWg.Wait() return err } // Create RPC client for remote dcrwallet instance (used for voting). + wallets := rpc.SetupWallet(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass, + cfg.WalletHosts, cfg.walletCert) // Dial once just to validate config. - walletConnect := make([]rpc.Connect, len(cfg.WalletHosts)) - walletConn := make([]rpc.Caller, len(cfg.WalletHosts)) - for i := 0; i < len(cfg.WalletHosts); i++ { - walletConnect[i] = rpc.Setup(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass, - cfg.WalletHosts[i], cfg.walletCert, nil) - walletConn[i], err = walletConnect[i]() - if err != nil { - log.Errorf("dcrwallet connection error: %v", err) - requestShutdown() - shutdownWg.Wait() - return err - } - _, err = rpc.WalletClient(ctx, walletConn[i], cfg.netParams.Params) - if err != nil { - log.Errorf("dcrwallet client error: %v", err) - requestShutdown() - shutdownWg.Wait() - return err - } + _, err = wallets.Clients(ctx, cfg.netParams.Params) + if err != nil { + log.Error(err) + requestShutdown() + shutdownWg.Wait() + return err } // Create a dcrd client with an attached notification handler which will run // in the background. notifHandler := &background.NotificationHandler{ - Ctx: ctx, - Db: db, - WalletConnect: walletConnect, - NetParams: cfg.netParams.Params, + Ctx: ctx, + Db: db, + Wallets: wallets, + NetParams: cfg.netParams.Params, } - dcrdWithNotifHandler := rpc.Setup(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass, + dcrdWithNotifHandler := rpc.SetupDcrd(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert, notifHandler) // Start background process which will continually attempt to reconnect to @@ -129,7 +111,7 @@ func run(ctx context.Context) error { VspClosed: cfg.VspClosed, } err = webapi.Start(ctx, shutdownRequestChannel, &shutdownWg, cfg.Listen, db, - dcrdConnect, walletConnect, cfg.WebServerDebug, cfg.FeeXPub, apiCfg) + dcrd, wallets, cfg.WebServerDebug, cfg.FeeXPub, apiCfg) if err != nil { log.Errorf("Failed to initialize webapi: %v", err) requestShutdown() diff --git a/rpc/client.go b/rpc/client.go index e1661d2..69bb73c 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -22,14 +22,16 @@ type Caller interface { Call(ctx context.Context, method string, res interface{}, args ...interface{}) error } -// Connect dials and returns a connected RPC client. -type Connect func() (Caller, error) +// connect dials and returns a connected RPC client. A boolean indicates whether +// this connection is new (true), or if it is an existing connection which is +// being reused (false). +type connect func() (Caller, bool, error) -// Setup accepts RPC connection details, creates an RPC client, and returns a +// setup accepts RPC connection details, creates an RPC client, and returns a // function which can be called to access the client. The returned function will // try to handle any client disconnects by attempting to reconnect, but will // return an error if a new connection cannot be established. -func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr string, cert []byte, n wsrpc.Notifier) Connect { +func setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr string, cert []byte, n wsrpc.Notifier) connect { // Create TLS options. pool := x509.NewCertPool() @@ -67,7 +69,7 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str shutdownWg.Done() }() - return func() (Caller, error) { + return func() (Caller, bool, error) { defer mu.Unlock() mu.Lock() @@ -77,16 +79,16 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str log.Debugf("RPC client %s errored (%v); reconnecting...", addr, c.Err()) c = nil default: - return c, nil + return c, false, nil } } var err error c, err = wsrpc.Dial(ctx, fullAddr, tlsOpt, authOpt, wsrpc.WithNotifier(n)) if err != nil { - return nil, err + return nil, false, err } - return c, nil + return c, true, nil } } diff --git a/rpc/dcrd.go b/rpc/dcrd.go index a58c332..4f8b780 100644 --- a/rpc/dcrd.go +++ b/rpc/dcrd.go @@ -5,12 +5,14 @@ import ( "encoding/hex" "errors" "fmt" + "sync" "github.com/decred/dcrd/blockchain/stake/v3" "github.com/decred/dcrd/chaincfg/v3" dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v2" "github.com/decred/dcrd/wire" "github.com/jrick/bitset" + "github.com/jrick/wsrpc/v2" ) const ( @@ -24,18 +26,36 @@ type DcrdRPC struct { ctx context.Context } -// DcrdClient creates a new DcrdRPC client instance from a caller. -func DcrdClient(ctx context.Context, c Caller, netParams *chaincfg.Params) (*DcrdRPC, error) { +type DcrdConnect connect + +func SetupDcrd(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr string, cert []byte, n wsrpc.Notifier) DcrdConnect { + return DcrdConnect(setup(ctx, shutdownWg, user, pass, addr, cert, n)) +} + +// Client creates a new DcrdRPC client instance. Returns an error if dialing +// dcrd fails or if dcrd is misconfigured. +func (d *DcrdConnect) Client(ctx context.Context, netParams *chaincfg.Params) (*DcrdRPC, error) { + + c, newConnection, err := connect(*d)() + if err != nil { + return nil, fmt.Errorf("dcrd connection error: %v", err) + } + + // If this is a reused connection, we don't need to validate the dcrd config + // again. + if !newConnection { + return &DcrdRPC{c, ctx}, nil + } // Verify dcrd is at the required api version. var verMap map[string]dcrdtypes.VersionResult - err := c.Call(ctx, "version", &verMap) + err = c.Call(ctx, "version", &verMap) if err != nil { - return nil, fmt.Errorf("version check failed: %v", err) + return nil, fmt.Errorf("dcrd version check failed: %v", err) } dcrdVersion, exists := verMap["dcrdjsonrpcapi"] if !exists { - return nil, fmt.Errorf("version response missing 'dcrdjsonrpcapi'") + return nil, fmt.Errorf("dcrd version response missing 'dcrdjsonrpcapi'") } if dcrdVersion.VersionString != requiredDcrdVersion { return nil, fmt.Errorf("wrong dcrd RPC version: got %s, expected %s", @@ -46,7 +66,7 @@ func DcrdClient(ctx context.Context, c Caller, netParams *chaincfg.Params) (*Dcr var netID wire.CurrencyNet err = c.Call(ctx, "getcurrentnet", &netID) if err != nil { - return nil, fmt.Errorf("getcurrentnet check failed: %v", err) + return nil, fmt.Errorf("dcrd getcurrentnet check failed: %v", err) } if netID != netParams.Net { return nil, fmt.Errorf("dcrd running on %s, expected %s", netID, netParams.Net) @@ -56,7 +76,7 @@ func DcrdClient(ctx context.Context, c Caller, netParams *chaincfg.Params) (*Dcr var info dcrdtypes.InfoChainResult err = c.Call(ctx, "getinfo", &info) if err != nil { - return nil, fmt.Errorf("getinfo check failed: %v", err) + return nil, fmt.Errorf("dcrd getinfo check failed: %v", err) } if !info.TxIndex { return nil, errors.New("dcrd does not have transaction index enabled (--txindex)") diff --git a/rpc/dcrwallet.go b/rpc/dcrwallet.go index ffd0417..94502e3 100644 --- a/rpc/dcrwallet.go +++ b/rpc/dcrwallet.go @@ -3,6 +3,7 @@ package rpc import ( "context" "fmt" + "sync" wallettypes "decred.org/dcrwallet/rpc/jsonrpc/types" "github.com/decred/dcrd/chaincfg/v3" @@ -21,56 +22,95 @@ type WalletRPC struct { ctx context.Context } -// WalletClient creates a new WalletRPC client instance from a caller. -func WalletClient(ctx context.Context, c Caller, netParams *chaincfg.Params) (*WalletRPC, error) { +type WalletConnect []connect - // Verify dcrwallet is at the required api version. - var verMap map[string]dcrdtypes.VersionResult - err := c.Call(ctx, "version", &verMap) - if err != nil { - return nil, fmt.Errorf("version check on dcrwallet '%s' failed: %v", - c.String(), err) - } - walletVersion, exists := verMap["dcrwalletjsonrpcapi"] - if !exists { - return nil, fmt.Errorf("version response on dcrwallet '%s' missing 'dcrwalletjsonrpcapi'", - c.String()) - } - if walletVersion.VersionString != requiredWalletVersion { - return nil, fmt.Errorf("dcrwallet '%s' has wrong RPC version: got %s, expected %s", - c.String(), walletVersion.VersionString, requiredWalletVersion) +func SetupWallet(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass string, addrs []string, cert []byte) WalletConnect { + walletConnect := make(WalletConnect, len(addrs)) + + for i := 0; i < len(addrs); i++ { + walletConnect[i] = setup(ctx, shutdownWg, user, pass, + addrs[i], cert, nil) } - // Verify dcrwallet is voting, unlocked, and is connected to dcrd (not SPV). - var walletInfo wallettypes.WalletInfoResult - err = c.Call(ctx, "walletinfo", &walletInfo) - if err != nil { - return nil, fmt.Errorf("walletinfo check on dcrwallet '%s' failed: %v", - c.String(), err) - } - if !walletInfo.Voting { - return nil, fmt.Errorf("wallet '%s' has voting disabled", c.String()) - } - if !walletInfo.Unlocked { - return nil, fmt.Errorf("wallet '%s' is not unlocked", c.String()) - } - if !walletInfo.DaemonConnected { - return nil, fmt.Errorf("wallet '%s' is not connected to dcrd", c.String()) + return walletConnect +} + +// Clients creates an array of new WalletRPC client instances. Returns an error +// if dialing any wallet fails, or if any wallet is misconfigured. +func (w *WalletConnect) Clients(ctx context.Context, netParams *chaincfg.Params) ([]*WalletRPC, error) { + walletClients := make([]*WalletRPC, len(*w)) + + for i := 0; i < len(*w); i++ { + + c, newConnection, err := []connect(*w)[i]() + if err != nil { + return nil, fmt.Errorf("dcrwallet connection error: %v", err) + } + + // If this is a reused connection, we don't need to validate the + // dcrwallet config again. + if !newConnection { + walletClients[i] = &WalletRPC{c, ctx} + continue + } + + // Verify dcrwallet is at the required api version. + var verMap map[string]dcrdtypes.VersionResult + err = c.Call(ctx, "version", &verMap) + if err != nil { + return nil, fmt.Errorf("version check on dcrwallet '%s' failed: %v", + c.String(), err) + } + walletVersion, exists := verMap["dcrwalletjsonrpcapi"] + if !exists { + return nil, fmt.Errorf("version response on dcrwallet '%s' missing 'dcrwalletjsonrpcapi'", + c.String()) + } + if walletVersion.VersionString != requiredWalletVersion { + return nil, fmt.Errorf("dcrwallet '%s' has wrong RPC version: got %s, expected %s", + c.String(), walletVersion.VersionString, requiredWalletVersion) + } + + // Verify dcrwallet is voting, unlocked, and is connected to dcrd (not SPV). + var walletInfo wallettypes.WalletInfoResult + err = c.Call(ctx, "walletinfo", &walletInfo) + if err != nil { + return nil, fmt.Errorf("walletinfo check on dcrwallet '%s' failed: %v", + c.String(), err) + } + + // TODO: The following 3 checks should probably just log a warning/error and + // not return. + // addtransaction and setvotechoice can still be used with a locked wallet. + // importprivkey will fail if wallet is locked. + + if !walletInfo.Voting { + return nil, fmt.Errorf("wallet '%s' has voting disabled", c.String()) + } + if !walletInfo.Unlocked { + return nil, fmt.Errorf("wallet '%s' is not unlocked", c.String()) + } + if !walletInfo.DaemonConnected { + return nil, fmt.Errorf("wallet '%s' is not connected to dcrd", c.String()) + } + + // Verify dcrwallet is on the correct network. + var netID wire.CurrencyNet + err = c.Call(ctx, "getcurrentnet", &netID) + if err != nil { + return nil, fmt.Errorf("getcurrentnet check on dcrwallet '%s' failed: %v", + c.String(), err) + } + if netID != netParams.Net { + return nil, fmt.Errorf("dcrwallet '%s' running on %s, expected %s", + c.String(), netID, netParams.Net) + } + + walletClients[i] = &WalletRPC{c, ctx} + } - // Verify dcrwallet is on the correct network. - var netID wire.CurrencyNet - err = c.Call(ctx, "getcurrentnet", &netID) - if err != nil { - return nil, fmt.Errorf("getcurrentnet check on dcrwallet '%s' failed: %v", - c.String(), err) - } - if netID != netParams.Net { - return nil, fmt.Errorf("dcrwallet '%s' running on %s, expected %s", - c.String(), netID, netParams.Net) - } - - return &WalletRPC{c, ctx}, nil + return walletClients, nil } func (c *WalletRPC) AddTransaction(blockHash, txHex string) error { diff --git a/webapi/middleware.go b/webapi/middleware.go index c795e3a..ccb8c24 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -16,43 +16,28 @@ type ticketHashRequest struct { // context for downstream handlers to make use of. func withDcrdClient() gin.HandlerFunc { return func(c *gin.Context) { - dcrdConn, err := dcrdConnect() + client, err := dcrd.Client(c, cfg.NetParams) if err != nil { - log.Errorf("dcrd connection error: %v", err) - sendErrorResponse("dcrd RPC error", http.StatusInternalServerError, c) - return - } - dcrdClient, err := rpc.DcrdClient(c, dcrdConn, cfg.NetParams) - if err != nil { - log.Errorf("dcrd client error: %v", err) + log.Error(err) sendErrorResponse("dcrd RPC error", http.StatusInternalServerError, c) return } - c.Set("DcrdClient", dcrdClient) + c.Set("DcrdClient", client) } } -// withWalletClient middleware adds a voting wallet client to the request +// withWalletClients middleware adds a voting wallet clients to the request // context for downstream handlers to make use of. -func withWalletClient() gin.HandlerFunc { +func withWalletClients() gin.HandlerFunc { return func(c *gin.Context) { - walletClient := make([]*rpc.WalletRPC, len(walletConnect)) - for i := 0; i < len(walletConnect); i++ { - walletConn, err := walletConnect[i]() - if err != nil { - log.Errorf("dcrwallet connection error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } - walletClient[i], err = rpc.WalletClient(c, walletConn, cfg.NetParams) - if err != nil { - log.Errorf("dcrwallet client error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } + clients, err := wallets.Clients(c, cfg.NetParams) + if err != nil { + log.Error(err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return } - c.Set("WalletClient", walletClient) + c.Set("WalletClients", clients) } } diff --git a/webapi/setvotechoices.go b/webapi/setvotechoices.go index 670635f..20628ff 100644 --- a/webapi/setvotechoices.go +++ b/webapi/setvotechoices.go @@ -17,7 +17,7 @@ func setVoteChoices(c *gin.Context) { rawRequest := c.MustGet("RawRequest").([]byte) ticket := c.MustGet("Ticket").(database.Ticket) knownTicket := c.MustGet("KnownTicket").(bool) - walletClients := c.MustGet("WalletClient").([]*rpc.WalletRPC) + walletClients := c.MustGet("WalletClients").([]*rpc.WalletRPC) if !knownTicket { log.Warnf("Invalid ticket from %s", c.ClientIP()) diff --git a/webapi/webapi.go b/webapi/webapi.go index 49afa16..fb112db 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -34,19 +34,19 @@ const ( var cfg Config var db *database.VspDatabase -var dcrdConnect rpc.Connect -var walletConnect []rpc.Connect +var dcrd rpc.DcrdConnect +var wallets rpc.WalletConnect var addrGen *addressGenerator var signPrivKey ed25519.PrivateKey var signPubKey ed25519.PublicKey func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *sync.WaitGroup, - listen string, vdb *database.VspDatabase, dConnect rpc.Connect, wConnect []rpc.Connect, debugMode bool, feeXPub string, config Config) error { + listen string, vdb *database.VspDatabase, dConnect rpc.DcrdConnect, wConnect rpc.WalletConnect, debugMode bool, feeXPub string, config Config) error { cfg = config db = vdb - dcrdConnect = dConnect - walletConnect = wConnect + dcrd = dConnect + wallets = wConnect var err error @@ -184,7 +184,7 @@ func router(debugMode bool) *gin.Engine { // These API routes access dcrd and the voting wallets, and they need // authentication. both := router.Group("/api").Use( - withDcrdClient(), withWalletClient(), vspAuth(), + withDcrdClient(), withWalletClients(), vspAuth(), ) both.POST("/setvotechoices", setVoteChoices)