Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,16 @@ A vspd deployment should have a minimum of three remote voting wallets. The
servers hosting these wallets should ideally be in geographically separate
locations.

Each voting server should be running an instance of dcrd and dcrwallet. The
wallet on these servers should be completely empty and not used for any purpose
other than voting tickets added by vspd.
dcrwallet should be permanently unlocked and have voting enabled
(`--enablevoting`). dcrwallet is also required to have the manual tickets
option (`--manualtickets`) enabled which disables dcrwallet adding tickets
arriving over the network.
This prevents a user from reusing a voting address and the VSP voting multiple
tickets with only a single fee payment.
vspd on the front-end server must be able to reach each instance of dcrwallet
over RPC.
Each voting server should be running an instance of dcrwallet backed by an
instance of dcrd (dcrwallet in SPV mode is not supported).
The wallet on these servers should be completely empty and not
used for any purpose other than voting tickets added by vspd. dcrwallet should
be permanently unlocked and have voting enabled (`--enablevoting`). dcrwallet is
also required to have the manual tickets option (`--manualtickets`) enabled
which disables dcrwallet adding tickets arriving over the network. This prevents
a user from reusing a voting address and the VSP voting multiple tickets with
only a single fee payment. vspd on the front-end server must be able to reach
each instance of dcrwallet over RPC.

## Front-end Server

Expand Down
74 changes: 46 additions & 28 deletions rpc/dcrd.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ import (
"github.com/jrick/wsrpc/v2"
)

var (
requiredDcrdVersion = semver{Major: 8, Minor: 3, Patch: 0}
)

const (
// These numerical error codes are defined in dcrd/dcrjson. Copied here so
// we dont need to import the whole package.
Expand Down Expand Up @@ -74,42 +70,28 @@ func (d *DcrdConnect) Close() {
// Client creates a new DcrdRPC client instance. Returns an error if dialing
// dcrd fails or if dcrd is misconfigured.
func (d *DcrdConnect) Client() (*DcrdRPC, string, error) {
ctx := context.TODO()
c, newConnection, err := d.client.dial(ctx)
c, newConnection, err := d.client.dial(context.TODO())
if err != nil {
return nil, d.client.addr, fmt.Errorf("dcrd dial error: %w", err)
}

dcrdRPC := &DcrdRPC{c}

// If this is a reused connection, we don't need to validate the dcrd config
// again.
if !newConnection {
return &DcrdRPC{c}, d.client.addr, nil
return dcrdRPC, d.client.addr, nil
}

// Verify dcrd is at the required api version.
var verMap map[string]dcrdtypes.VersionResult
err = c.Call(ctx, "version", &verMap)
// Verify dcrd is at the required version.
err = dcrdRPC.checkVersion()
if err != nil {
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd version check failed: %w", err)
}

ver, exists := verMap["dcrdjsonrpcapi"]
if !exists {
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd version response missing 'dcrdjsonrpcapi'")
}

sVer := semver{ver.Major, ver.Minor, ver.Patch}
if !semverCompatible(requiredDcrdVersion, sVer) {
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd has incompatible JSON-RPC version: got %s, expected %s",
sVer, requiredDcrdVersion)
}

// Verify dcrd is on the correct network.
var netID wire.CurrencyNet
err = c.Call(ctx, "getcurrentnet", &netID)
netID, err := dcrdRPC.getCurrentNet()
if err != nil {
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd getcurrentnet check failed: %w", err)
Expand All @@ -120,8 +102,7 @@ func (d *DcrdConnect) Client() (*DcrdRPC, string, error) {
}

// Verify dcrd has tx index enabled (required for getrawtransaction).
var info dcrdtypes.InfoChainResult
err = c.Call(ctx, "getinfo", &info)
info, err := dcrdRPC.getInfo()
if err != nil {
d.client.Close()
return nil, d.client.addr, fmt.Errorf("dcrd getinfo check failed: %w", err)
Expand All @@ -133,7 +114,7 @@ func (d *DcrdConnect) Client() (*DcrdRPC, string, error) {

// Request blockconnected notifications.
if d.client.notifier != nil {
err = c.Call(ctx, "notifyblocks", nil)
err = dcrdRPC.NotifyBlocks()
if err != nil {
return nil, d.client.addr, fmt.Errorf("notifyblocks failed: %w", err)
}
Expand All @@ -144,6 +125,43 @@ func (d *DcrdConnect) Client() (*DcrdRPC, string, error) {
return &DcrdRPC{c}, d.client.addr, nil
}

// checkVersion uses version RPC to retrieve the binary and API version of dcrd.
// An error is returned if there is not semver compatibility with the minimum
// expected versions.
func (c *DcrdRPC) checkVersion() error {
var verMap map[string]dcrdtypes.VersionResult
err := c.Call(context.TODO(), "version", &verMap)
if err != nil {
return err
}

return errors.Join(
checkVersion(verMap, "dcrd"),
checkVersion(verMap, "dcrdjsonrpcapi"),
)
}

// getCurrentNet uses getcurrentnet RPC to return the Decred network the wallet
// is connected to.
func (c *DcrdRPC) getCurrentNet() (wire.CurrencyNet, error) {
var netID wire.CurrencyNet
err := c.Call(context.TODO(), "getcurrentnet", &netID)
if err != nil {
return 0, err
}
return netID, nil
}

// getInfo uses getinfo RPC to return various daemon, network, and chain info.
func (c *DcrdRPC) getInfo() (*dcrdtypes.InfoChainResult, error) {
var info dcrdtypes.InfoChainResult
err := c.Call(context.TODO(), "getinfo", &info)
if err != nil {
return nil, err
}
return &info, nil
}

// GetRawTransaction uses getrawtransaction RPC to retrieve details about the
// transaction with the provided hash.
func (c *DcrdRPC) GetRawTransaction(txHash string) (*dcrdtypes.TxRawResult, error) {
Expand Down
71 changes: 39 additions & 32 deletions rpc/dcrwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package rpc

import (
"context"
"errors"
"fmt"

wallettypes "decred.org/dcrwallet/v5/rpc/jsonrpc/types"
Expand All @@ -15,10 +16,6 @@ import (
"github.com/decred/slog"
)

var (
requiredWalletVersion = semver{Major: 11, Minor: 0, Patch: 0}
)

// WalletRPC provides methods for calling dcrwallet JSON-RPCs without exposing the details
// of JSON encoding.
type WalletRPC struct {
Expand Down Expand Up @@ -56,57 +53,38 @@ func (w *WalletConnect) Close() {
// increments a count of failed connections if a connection cannot be
// established, or if the wallet is misconfigured.
func (w *WalletConnect) Clients() ([]*WalletRPC, []string) {
ctx := context.TODO()
walletClients := make([]*WalletRPC, 0)
failedConnections := make([]string, 0)

for _, connect := range w.clients {

c, newConnection, err := connect.dial(ctx)
c, newConnection, err := connect.dial(context.TODO())
if err != nil {
w.log.Errorf("dcrwallet dial error: %v", err)
failedConnections = append(failedConnections, connect.addr)
continue
}

walletRPC := &WalletRPC{c}

// If this is a reused connection, we don't need to validate the
// dcrwallet config again.
if !newConnection {
walletClients = append(walletClients, &WalletRPC{c})
walletClients = append(walletClients, walletRPC)
continue
}

// Verify dcrwallet is at the required api version.
var verMap map[string]dcrdtypes.VersionResult
err = c.Call(ctx, "version", &verMap)
// Verify dcrwallet and dcrd are at the required versions.
err = walletRPC.checkVersions()
if err != nil {
w.log.Errorf("dcrwallet.Version error (wallet=%s): %v", c.String(), err)
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue
}

ver, exists := verMap["dcrwalletjsonrpcapi"]
if !exists {
w.log.Errorf("dcrwallet.Version response missing 'dcrwalletjsonrpcapi' (wallet=%s)",
c.String())
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue
}

sVer := semver{ver.Major, ver.Minor, ver.Patch}
if !semverCompatible(requiredWalletVersion, sVer) {
w.log.Errorf("dcrwallet has incompatible JSON-RPC version (wallet=%s): got %s, expected %s",
c.String(), sVer, requiredWalletVersion)
w.log.Errorf("Version check failed (wallet=%s): %v", c.String(), err)
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue
}

// Verify dcrwallet is on the correct network.
var netID wire.CurrencyNet
err = c.Call(ctx, "getcurrentnet", &netID)
netID, err := walletRPC.getCurrentNet()
if err != nil {
w.log.Errorf("dcrwallet.GetCurrentNet error (wallet=%s): %v", c.String(), err)
failedConnections = append(failedConnections, connect.addr)
Expand All @@ -122,7 +100,6 @@ func (w *WalletConnect) Clients() ([]*WalletRPC, []string) {
}

// Verify dcrwallet is voting and unlocked.
walletRPC := &WalletRPC{c}
walletInfo, err := walletRPC.WalletInfo()
if err != nil {
w.log.Errorf("dcrwallet.WalletInfo error (wallet=%s): %v", c.String(), err)
Expand Down Expand Up @@ -155,6 +132,36 @@ func (w *WalletConnect) Clients() ([]*WalletRPC, []string) {
return walletClients, failedConnections
}

// checkVersion uses version RPC to retrieve the binary and API versions
// dcrwallet and its backing dcrd. An error is returned if there is not semver
// compatibility with the minimum expected versions.
func (c *WalletRPC) checkVersions() error {
var verMap map[string]dcrdtypes.VersionResult
err := c.Call(context.TODO(), "version", &verMap)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of introducing even more TODO, why not write this with context support and pass in TODO from the caller, if that is still needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might just be naive here, but I believe the reality of the situation is that context just isn't needed in these RPC clients.

None of the calls are particularly long running, and in every case I think adding early interruption adds more complexity than its worth. The code is kept cleaner and data integrity is protected if we allow vspd to finish what it is doing before shutting down.

The one thing I might consider is adding a timeout to the context, but the underlying context would still be TODO or Background anyway.

if err != nil {
return err
}

// Presence of dcrd and dcrdjsonrpcapi in this map confirms dcrwallet is not
// running in SPV mode.
return errors.Join(
checkVersion(verMap, "dcrd"),
checkVersion(verMap, "dcrdjsonrpcapi"),
checkVersion(verMap, "dcrwallet"),
checkVersion(verMap, "dcrwalletjsonrpcapi"),
)
}

// getCurrentNet returns the Decred network the wallet is connected to.
func (c *WalletRPC) getCurrentNet() (wire.CurrencyNet, error) {
var netID wire.CurrencyNet
err := c.Call(context.TODO(), "getcurrentnet", &netID)
if err != nil {
return 0, err
}
return netID, nil
}

// WalletInfo uses walletinfo RPC to retrieve information about how the
// dcrwallet instance is configured.
func (c *WalletRPC) WalletInfo() (*wallettypes.WalletInfoResult, error) {
Expand Down
30 changes: 0 additions & 30 deletions rpc/semver.go

This file was deleted.

61 changes: 61 additions & 0 deletions rpc/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) 2020-2025 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package rpc

import (
"fmt"

dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
)

// minimumVersions contains the minimum expected binary and API versions for
// dcrd and dcrwallet.
var minimumVersions = map[string]semver{
"dcrd": {Major: 2, Minor: 1},
"dcrdjsonrpcapi": {Major: 8, Minor: 3},
"dcrwallet": {Major: 2, Minor: 1},
"dcrwalletjsonrpcapi": {Major: 11, Minor: 0},
}

// checkVersion returns an error if the provided key in verMap does not have
// semver compatibility with the minimum expected versions.
func checkVersion(verMap map[string]dcrdtypes.VersionResult, key string) error {
var actualV semver
if ver, ok := verMap[key]; ok {
actualV = semver{ver.Major, ver.Minor, ver.Patch}
} else {
return fmt.Errorf("version map missing key %q", key)
}

minimumV := minimumVersions[key]
if !semverCompatible(minimumV, actualV) {
return fmt.Errorf("incompatible %q version, expected %s got %s",
key, minimumV, actualV)
}
return nil
}

type semver struct {
Major uint32
Minor uint32
Patch uint32
}

func semverCompatible(required, actual semver) bool {
switch {
case required.Major != actual.Major:
return false
case required.Minor > actual.Minor:
return false
case required.Minor == actual.Minor && required.Patch > actual.Patch:
return false
default:
return true
}
}

func (s semver) String() string {
return fmt.Sprintf("%d.%d.%d", s.Major, s.Minor, s.Patch)
}
Loading