Repository: decred/dcrd
Pull request #2549 (2021-01-12 11:02:04)

rpcserver: add handleVersion, handleHelp rpc tests.
This adds rpc tests for `handleStop`, `handleHelp`, `handleGetStakeDifficulty` and `handleVersion`.

Work towards #2069.

dnldd: ```sh
❯ go tool cover -func=cov.out | grep -E "handleVersion"
github.com/decred/dcrd/internal/rpcserver/rpcserver.go:5374:    handleVersion                    100.0%

❯ go tool cover -func=cov.out | grep -E "handleGetStakeDifficulty"
github.com/decred/dcrd/internal/rpcserver/rpcserver.go:2859:    handleGetStakeDifficulty	 100.0%

❯ go tool cover -func=cov.out | grep -E "handleStop"   
github.com/decred/dcrd/internal/rpcserver/rpcserver.go:4714:    handleStop                       100.0%

❯ go tool cover -func=cov.out | grep -E "handleHelp" 
github.com/decred/dcrd/internal/rpcserver/rpcserver.go:3859:    handleHelp                       100.0%
```

Patch:
diff --git a/blockchain/chain.go b/blockchain/chain.go
index 8a4043bd..1c89a218 100644
--- a/blockchain/chain.go
+++ b/blockchain/chain.go
@@ -1961,7 +1961,7 @@ func stxosToScriptSource(block *dcrutil.Block, stxos []spentTxOut, isTreasuryEna
 			prevOut := &txIn.PreviousOutPoint
 			source[*prevOut] = scriptSourceEntry{
 				version: stxo.scriptVersion,
-				script:  decompressScript(stxo.pkScript),
+				script:  stxo.pkScript,
 			}
 		}
 	}
@@ -1983,7 +1983,7 @@ func stxosToScriptSource(block *dcrutil.Block, stxos []spentTxOut, isTreasuryEna
 			prevOut := &txIn.PreviousOutPoint
 			source[*prevOut] = scriptSourceEntry{
 				version: stxo.scriptVersion,
-				script:  decompressScript(stxo.pkScript),
+				script:  stxo.pkScript,
 			}
 		}
 	}
diff --git a/blockchain/upgrade.go b/blockchain/upgrade.go
index ccae1888..04846492 100644
--- a/blockchain/upgrade.go
+++ b/blockchain/upgrade.go
@@ -2337,7 +2337,7 @@ func migrateSpendJournalVersion2To3(ctx context.Context, b *BlockChain) error {
 	v2SpendJournalBucketName := []byte("spendjournalv2")
 	v3SpendJournalBucketName := []byte("spendjournalv3")
 	v3BlockIndexBucketName := []byte("blockidxv3")
-	v3UtxoSetBucketName := []byte("utxosetv3")
+	v2UtxoSetBucketName := []byte("utxosetv2")
 	tmpTxInfoBucketName := []byte("tmpTxInfo")
 
 	log.Info("Migrating database spend journal.  This may take a while...")
@@ -2392,10 +2392,10 @@ func migrateSpendJournalVersion2To3(ctx context.Context, b *BlockChain) error {
 				v3BlockIndexBucketName)
 		}
 
-		v3UtxoSetBucket := meta.Bucket(v3UtxoSetBucketName)
-		if v3UtxoSetBucket == nil {
+		v2UtxoSetBucket := meta.Bucket(v2UtxoSetBucketName)
+		if v2UtxoSetBucket == nil {
 			return false, fmt.Errorf("bucket %s does not exist",
-				v3UtxoSetBucketName)
+				v2UtxoSetBucketName)
 		}
 
 		tmpTxInfoBucket := meta.Bucket(tmpTxInfoBucketName)
@@ -2647,6 +2647,10 @@ func migrateSpendJournalVersion2To3(ctx context.Context, b *BlockChain) error {
 						txType := stake.TxType((flags & 0x1c) >> 2)
 						stxo.flags = uint8(flags)
 
+						// Unset bit 5 in case it was unexpectedly set, since bit 5 will be
+						// used for the transaction type in the new format.
+						stxo.flags &^= 1 << 5
+
 						// Unset the spent flag since it is no longer needed in the new
 						// version.
 						stxo.flags &^= 1 << 6
@@ -2742,173 +2746,207 @@ func migrateSpendJournalVersion2To3(ctx context.Context, b *BlockChain) error {
 					}
 
 					// If the temp bucket didn't have the tx info, check the utxo set for
-					// the tx info.  We don't know what output to look for, so this scans
-					// until it finds the first output for the corresponding transaction
-					// hash.
-					//
-					// Attempt to find an entry by seeking for the hash along with a zero
-					// index and tree.  Due to the fact the keys are serialized as
-					// <hash><tree><index>, where the tree and index use an MSB encoding,
-					// if there are any entries for the hash at all, one will be found.
-					cursor := v3UtxoSetBucket.Cursor()
-					tree := uint64(0)
-					index := uint64(0)
-					utxoKey := make([]byte, chainhash.HashSize+serializeSizeVLQ(tree)+
-						serializeSizeVLQ(index))
-					copy(utxoKey, txIn.PreviousOutPoint.Hash[:])
-					utxoKeyOffset := chainhash.HashSize
-					utxoKeyOffset += putVLQ(utxoKey[utxoKeyOffset:], tree)
-					putVLQ(utxoKey[utxoKeyOffset:], index)
-					ok := cursor.Seek(utxoKey)
-					if !ok {
-						return false, AssertError(fmt.Sprintf("Couldn't find any utxo "+
-							"for non-fully spent tx %v",
-							txIn.PreviousOutPoint.Hash.String()))
-					}
-					cursorKey := cursor.Key()
-					if len(cursorKey) < chainhash.HashSize {
-						return false, AssertError(fmt.Sprintf("Couldn't find any utxo "+
-							"for non-fully spent tx %v",
-							txIn.PreviousOutPoint.Hash.String()))
-					}
+					// the tx info.  The key for the V2 utxo set is the transaction hash.
+					utxoSerialized := v2UtxoSetBucket.Get(txIn.PreviousOutPoint.Hash[:])
 
-					// An entry was found, but it could just be an entry with the next
-					// highest hash after the requested one, so make sure the hashes
-					// actually match.
-					if chainhash.HashSize >= len(cursorKey) {
-						return false, errDeserialize("unexpected length for serialized " +
-							"outpoint key")
-					}
-					var keyHash chainhash.Hash
-					copy(keyHash[:], cursorKey[:chainhash.HashSize])
-					keyOffset := chainhash.HashSize
-					keyTree, bytesRead := deserializeVLQ(cursorKey[keyOffset:])
-					keyOffset += bytesRead
-					if keyOffset >= len(cursorKey) {
-						return false, errDeserialize("unexpected end of data after tree")
-					}
-					keyIdx, _ := deserializeVLQ(cursorKey[keyOffset:])
-					outpoint := wire.OutPoint{Hash: keyHash, Index: uint32(keyIdx),
-						Tree: int8(keyTree)}
-					if outpoint.Hash != txIn.PreviousOutPoint.Hash {
-						return false, AssertError(fmt.Sprintf("Couldn't find any utxo "+
-							"for non-fully spent tx %v",
-							txIn.PreviousOutPoint.Hash.String()))
-					}
-
-					// Deserialize the V3 utxo entry.
-					//
-					// The V3 serialized key format is:
+					// Deserialize the legacy V2 entry which included all utxos for the
+					// given transaction.
 					//
-					//   <hash><tree><output index>
+					// The legacy V2 format is as follows:
 					//
-					//   Field                Type             Size
-					//   hash                 chainhash.Hash   chainhash.HashSize
-					//   tree                 VLQ              variable
-					//   output index         VLQ              variable
+					//   <version><height><header code><unspentness bitmap>
+					//	 [<compressed txouts>,...]
 					//
-					// The V3 serialized value format is:
-					//
-					//   <block height><block index><flags><compressed txout>
-					//   OPTIONAL: [<ticket min outs>]
-					//
-					//   Field                Type     Size
-					//   block height         VLQ      variable
-					//   block index          VLQ      variable
-					//   flags                VLQ      variable
-					//   compressed txout
+					//   Field                 Type     Size
+					//   transaction version   VLQ      variable
+					//   block height          VLQ      variable
+					//   block index           VLQ      variable
+					//   flags                 VLQ      variable (currently 1 byte)
+					//   header code           VLQ      variable
+					//   unspentness bitmap    []byte   variable
+					//   compressed txouts
 					//     compressed amount   VLQ      variable
 					//     script version      VLQ      variable
 					//     compressed script   []byte   variable
+					//   stakeExtra            []byte   variable
 					//
-					//   OPTIONAL
-					//     ticketMinOuts      []byte         variable
+					// The serialized flags code format is:
+					//   bit  0   - containing transaction is a coinbase
+					//   bit  1   - containing transaction has an expiry
+					//   bits 2-4 - transaction type
+					//   bit  5   - unused
+					//   bit  6   - is fully spent
+					//   bit  7   - unused
 					//
-					// The serialized flags format is:
-					//   bit  0     - containing transaction is a coinbase
-					//   bit  1     - containing transaction has an expiry
-					//   bits 2-5   - transaction type
-					//   bits 6-7   - unused
+					// The serialized header code format is:
+					//   bit 0 - output zero is unspent
+					//   bit 1 - output one is unspent
+					//   bits 2-x - number of bytes in unspentness bitmap.  When both bits
+					//     1 and 2 are unset, it encodes N-1 since there must be at least
+					//     one unspent output.
 					//
-					// The ticket min outs field contains minimally encoded outputs for
-					// all outputs of a ticket transaction. It is only encoded for ticket
-					// submission outputs.
-					utxoSerialized := cursor.Value()
+					// The stake extra field contains minimally encoded outputs for all
+					// consensus-related outputs in the stake transaction. It is only
+					// encoded for tickets.
 
-					// Deserialize the block height.  Ignore it since we don't need it.
+					// Deserialize the version.  Ignore it since we don't need it.
 					_, bytesRead = deserializeVLQ(utxoSerialized)
 					utxoOffset := bytesRead
+					if utxoOffset >= len(utxoSerialized) {
+						return false, errDeserialize("unexpected end of data after version")
+					}
+
+					// Deserialize the block height.  Ignore it since we don't need it.
+					_, bytesRead = deserializeVLQ(utxoSerialized[utxoOffset:])
+					utxoOffset += bytesRead
 					if utxoOffset >= len(utxoSerialized) {
 						return false, errDeserialize("unexpected end of data after height")
 					}
 
-					// Deserialize the block index.  Ignore it since we don't need it.
+					// Deserialize the block index. Ignore it since we don't need it.
 					_, bytesRead = deserializeVLQ(utxoSerialized[utxoOffset:])
 					utxoOffset += bytesRead
 					if utxoOffset >= len(utxoSerialized) {
 						return false, errDeserialize("unexpected end of data after index")
 					}
 
-					// Deserialize the flags.
-					v3UtxoFlags, bytesRead := deserializeVLQ(utxoSerialized[utxoOffset:])
+					// Deserialize the flags.  The flags format is:
+					//     0: Is coinbase
+					//     1: Has an expiry
+					//   2-4: Transaction type
+					//     5: Unused
+					//     6: Fully spent
+					//     7: Unused
+					v2UtxoFlags, bytesRead := deserializeVLQ(utxoSerialized[utxoOffset:])
 					utxoOffset += bytesRead
 					if utxoOffset >= len(utxoSerialized) {
 						return false, errDeserialize("unexpected end of data after flags")
 					}
 
+					// Unset bit 5 in case it was unexpectedly set, since bit 5 will be
+					// used for the transaction type in the new format.
+					v2UtxoFlags &^= 1 << 5
+
+					// Unset the fully spent flag since it is no longer needed in the new
+					// version.  It shouldn't have ever been set, since spent utxos are
+					// not serialized, but unset it just in case.
+					v2UtxoFlags &^= 1 << 6
+
 					// Set the flags on the stxo.
-					stxo.flags = uint8(v3UtxoFlags)
+					stxo.flags = uint8(v2UtxoFlags)
 
-					// Deserialize the compressed amount and ensure there are bytes
-					// remaining for the compressed script.  Ignore it since we don't need
-					// it.
-					_, bytesRead = deserializeVLQ(utxoSerialized[utxoOffset:])
-					if bytesRead == 0 {
-						return false, errDeserialize("unexpected end of data during " +
-							"decoding (compressed amount)")
-					}
+					// Deserialize the header code.
+					code, bytesRead := deserializeVLQ(utxoSerialized[utxoOffset:])
 					utxoOffset += bytesRead
+					if utxoOffset >= len(utxoSerialized) {
+						return false, errDeserialize("unexpected end of data after header")
+					}
 
-					// Decode the script version.  Ignore it since we don't need it.
-					_, bytesRead = deserializeVLQ(utxoSerialized[utxoOffset:])
-					if bytesRead == 0 {
-						return false, errDeserialize("unexpected end of data during " +
-							"decoding (script version)")
+					// Decode the header code.
+					//
+					// Bit 0 indicates output 0 is unspent.
+					// Bit 1 indicates output 1 is unspent.
+					// Bits 2-x encodes the number of non-zero unspentness bitmap bytes
+					// that follow.  When both output 0 and 1 are spent, it encodes N-1.
+					output0Unspent := code&0x01 != 0
+					output1Unspent := code&0x02 != 0
+					numBitmapBytes := code >> 2
+					if !output0Unspent && !output1Unspent {
+						numBitmapBytes++
 					}
-					utxoOffset += bytesRead
 
-					// Decode the compressed script size and ensure there are enough bytes
-					// left in the slice for it.
-					scriptSize = decodeCompressedScriptSizeV1(utxoSerialized[utxoOffset:])
-					// Note: scriptSize == 0 is OK (an empty compressed script is valid)
-					if scriptSize < 0 {
-						return false, errDeserialize("negative script size")
+					// Ensure there are enough bytes left to deserialize the unspentness
+					// bitmap.
+					if uint64(len(utxoSerialized[utxoOffset:])) < numBitmapBytes {
+						return false, errDeserialize("unexpected end of data for " +
+							"unspentness bitmap")
 					}
-					if len(utxoSerialized[utxoOffset:]) < scriptSize {
-						return false, errDeserialize(fmt.Sprintf("unexpected end of "+
-							"data after script size (got %v, need %v)",
-							len(utxoSerialized[utxoOffset:]), scriptSize))
+
+					// Add sparse outputs for unspent outputs 0 and 1 as needed based on
+					// the details provided by the header code.
+					var outputIndexes []uint32
+					if output0Unspent {
+						outputIndexes = append(outputIndexes, 0)
+					}
+					if output1Unspent {
+						outputIndexes = append(outputIndexes, 1)
+					}
+
+					// Decode the unspentness bitmap adding a sparse output for each
+					// unspent output.
+					for i := uint32(0); i < uint32(numBitmapBytes); i++ {
+						unspentBits := utxoSerialized[utxoOffset]
+						for j := uint32(0); j < 8; j++ {
+							if unspentBits&0x01 != 0 {
+								// The first 2 outputs are encoded via the
+								// header code, so adjust the output number
+								// accordingly.
+								outputNum := 2 + i*8 + j
+								outputIndexes = append(outputIndexes, outputNum)
+							}
+							unspentBits >>= 1
+						}
+						utxoOffset++
+					}
+
+					// Decode and add all of the outputs.
+					for range outputIndexes {
+						// Deserialize the compressed amount.  Ignore it since we don't need
+						// it.
+						_, bytesRead = deserializeVLQ(utxoSerialized[utxoOffset:])
+						if bytesRead == 0 {
+							return false, errDeserialize("unexpected end of data during " +
+								"decoding (compressed amount)")
+						}
+						utxoOffset += bytesRead
+
+						// Decode the script version.  Ignore it since we don't need it.
+						_, bytesRead = deserializeVLQ(utxoSerialized[utxoOffset:])
+						if bytesRead == 0 {
+							return false, errDeserialize("unexpected end of data during " +
+								"decoding (script version)")
+						}
+						utxoOffset += bytesRead
+
+						// Decode the compressed script size and ensure there are enough
+						// bytes left in the slice for it.
+						size := decodeCompressedScriptSizeV1(utxoSerialized[utxoOffset:])
+						// Note: size == 0 is OK (an empty compressed script is valid)
+						if size < 0 {
+							return false, errDeserialize("negative script size")
+						}
+						if len(utxoSerialized[utxoOffset:]) < size {
+							return false, errDeserialize(fmt.Sprintf("unexpected end of "+
+								"data after script size (got %v, need %v)",
+								len(utxoSerialized[utxoOffset:]), size))
+						}
+
+						utxoOffset += size
 					}
-					// Ignore the script since we don't need it.
-					utxoOffset += scriptSize
 
 					// Determine the tx type from the flags.  The flags format is:
-					//   bit  0     - containing transaction is a coinbase
-					//   bit  1     - containing transaction has an expiry
-					//   bits 2-5   - transaction type
-					//   bits 6-7   - unused
-					txType := stake.TxType((v3UtxoFlags & 0x3c) >> 2)
-
-					// Read the minimal outputs if this was a ticket submission output.
-					if txType == stake.TxTypeSStx && outpoint.Index == 0 {
+					//     0: Is coinbase
+					//     1: Has an expiry
+					//   2-4: Transaction type
+					//     5: Unused
+					//     6: Fully spent
+					//     7: Unused
+					txType := stake.TxType((v2UtxoFlags & 0x1c) >> 2)
+
+					// Read the minimal outputs if this was a ticket.
+					if txType == stake.TxTypeSStx {
 						sz, err := determineMinimalOutputsSizeV1(utxoSerialized[utxoOffset:])
 						if err != nil {
 							return false, errDeserialize(fmt.Sprintf("unable to decode "+
 								"ticket outputs: %v", err))
 						}
 
-						// Read the ticket minimal outputs.
-						stxo.ticketMinOuts = utxoSerialized[utxoOffset : utxoOffset+sz]
+						// Read the ticket minimal outputs.  We only need the ticket minimal
+						// outputs for the ticket submission output (output 0) in the new
+						// format.
+						if stxo.txOutIndex == 0 {
+							stxo.ticketMinOuts = utxoSerialized[utxoOffset : utxoOffset+sz]
+						}
 					}
 				}
 			}
@@ -2986,6 +3024,11 @@ func migrateSpendJournalVersion2To3(ctx context.Context, b *BlockChain) error {
 				// Only store the minimal outputs if this was a ticket submission
 				// output.
 				if txType == stake.TxTypeSStx && stxo.txOutIndex == 0 {
+					if len(stxo.ticketMinOuts) == 0 {
+						return false, errDeserialize("missing ticket minimal output data " +
+							"when serializing V3 stxo entry")
+					}
+
 					copy(reserialized[offset:], stxo.ticketMinOuts)
 					offset += len(stxo.ticketMinOuts)
 				}
@@ -3058,10 +3101,10 @@ func upgradeToVersion9(ctx context.Context, b *BlockChain) error {
 	log.Info("Upgrading database to version 9...")
 	start := time.Now()
 
-	// Migrate the utxoset to version 3.
-	v3DoneKeyName := []byte("utxosetv3done")
+	// Migrate the spend journal to version 3.
+	v3DoneKeyName := []byte("spendjournalv3done")
 	err := runUpgradeStageOnce(ctx, b.db, v3DoneKeyName, func() error {
-		return migrateUtxoSetVersion2To3(ctx, b.db)
+		return migrateSpendJournalVersion2To3(ctx, b)
 	})
 	if err != nil {
 		return err
@@ -3071,13 +3114,13 @@ func upgradeToVersion9(ctx context.Context, b *BlockChain) error {
 		return errInterruptRequested
 	}
 
-	// Migrate the spend journal to version 3.
-	err = migrateSpendJournalVersion2To3(ctx, b)
+	// Migrate the utxoset to version 3.
+	err = migrateUtxoSetVersion2To3(ctx, b.db)
 	if err != nil {
 		return err
 	}
 
-	// Update and persist the database version  and remove upgrade progress
+	// Update and persist the database version and remove upgrade progress
 	// tracking keys.
 	err = b.db.Update(func(dbTx database.Tx) error {
 		err := dbTx.Metadata().Delete(v3DoneKeyName)
diff --git a/internal/rpcserver/interface.go b/internal/rpcserver/interface.go
index 2fe00f8c..dc3ebc24 100644
--- a/internal/rpcserver/interface.go
+++ b/internal/rpcserver/interface.go
@@ -21,6 +21,7 @@ import (
 	"github.com/decred/dcrd/internal/mempool"
 	"github.com/decred/dcrd/internal/mining"
 	"github.com/decred/dcrd/peer/v2"
+	"github.com/decred/dcrd/rpc/jsonrpc/types/v3"
 	"github.com/decred/dcrd/wire"
 )
 
@@ -734,3 +735,16 @@ type NtfnManager interface {
 	// cancelled.
 	Run(ctx context.Context)
 }
+
+// RPCHelpCacher represents a cacher that provides help and usage text for RPC
+// server commands and caches the results.
+//
+// The interface contract requires that all of these methods are safe for
+// concurrent access.
+type RPCHelpCacher interface {
+	// RPCMethodHelp returns an RPC help string for the provided method.
+	RPCMethodHelp(method types.Method) (string, error)
+
+	// RPCUsage returns one-line usage for all supported RPC commands.
+	RPCUsage(includeWebsockets bool) (string, error)
+}
diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go
index 9dbd8eb2..4426b069 100644
--- a/internal/rpcserver/rpcserver.go
+++ b/internal/rpcserver/rpcserver.go
@@ -3869,7 +3869,7 @@ func handleHelp(_ context.Context, s *Server, cmd interface{}) (interface{}, err
 		method = types.Method(*c.Command)
 	}
 	if method == "" {
-		usage, err := s.helpCacher.rpcUsage(false)
+		usage, err := s.helpCacher.RPCUsage(false)
 		if err != nil {
 			context := "Failed to generate RPC usage"
 			return nil, rpcInternalError(err.Error(), context)
@@ -3886,7 +3886,7 @@ func handleHelp(_ context.Context, s *Server, cmd interface{}) (interface{}, err
 	}
 
 	// Get the help for the command.
-	help, err := s.helpCacher.rpcMethodHelp(method)
+	help, err := s.helpCacher.RPCMethodHelp(method)
 	if err != nil {
 		context := "Failed to generate help"
 		return nil, rpcInternalError(err.Error(), context)
@@ -5415,7 +5415,7 @@ type Server struct {
 	statusLock             sync.RWMutex
 	wg                     sync.WaitGroup
 	workState              *workState
-	helpCacher             *helpCacher
+	helpCacher             RPCHelpCacher
 	requestProcessShutdown chan struct{}
 }
 
diff --git a/internal/rpcserver/rpcserverhelp.go b/internal/rpcserver/rpcserverhelp.go
index 178d8225..442983cb 100644
--- a/internal/rpcserver/rpcserverhelp.go
+++ b/internal/rpcserver/rpcserverhelp.go
@@ -1095,10 +1095,10 @@ type helpCacher struct {
 	methodHelp map[types.Method]string
 }
 
-// rpcMethodHelp returns an RPC help string for the provided method.
+// RPCMethodHelp returns an RPC help string for the provided method.
 //
 // This function is safe for concurrent access.
-func (c *helpCacher) rpcMethodHelp(method types.Method) (string, error) {
+func (c *helpCacher) RPCMethodHelp(method types.Method) (string, error) {
 	c.Lock()
 	defer c.Unlock()
 
@@ -1123,10 +1123,10 @@ func (c *helpCacher) rpcMethodHelp(method types.Method) (string, error) {
 	return help, nil
 }
 
-// rpcUsage returns one-line usage for all support RPC commands.
+// RPCUsage returns one-line usage for all supported RPC commands.
 //
 // This function is safe for concurrent access.
-func (c *helpCacher) rpcUsage(includeWebsockets bool) (string, error) {
+func (c *helpCacher) RPCUsage(includeWebsockets bool) (string, error) {
 	c.Lock()
 	defer c.Unlock()
 
diff --git a/internal/rpcserver/rpcwebsocket.go b/internal/rpcserver/rpcwebsocket.go
index 58e4a021..846b206c 100644
--- a/internal/rpcserver/rpcwebsocket.go
+++ b/internal/rpcserver/rpcwebsocket.go
@@ -2070,7 +2070,7 @@ func handleWebsocketHelp(wsc *wsClient, icmd interface{}) (interface{}, error) {
 		method = types.Method(*cmd.Command)
 	}
 	if method == "" {
-		usage, err := wsc.rpcServer.helpCacher.rpcUsage(true)
+		usage, err := wsc.rpcServer.helpCacher.RPCUsage(true)
 		if err != nil {
 			context := "Failed to generate RPC usage"
 			return nil, rpcInternalError(err.Error(), context)
@@ -2093,7 +2093,7 @@ func handleWebsocketHelp(wsc *wsClient, icmd interface{}) (interface{}, error) {
 	}
 
 	// Get the help for the command.
-	help, err := wsc.rpcServer.helpCacher.rpcMethodHelp(method)
+	help, err := wsc.rpcServer.helpCacher.RPCMethodHelp(method)
 	if err != nil {
 		context := "Failed to generate help"
 		return nil, rpcInternalError(err.Error(), context)


Test patch:
diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go
index 5cbe276a..fb92221f 100644
--- a/internal/rpcserver/rpcserverhandlers_test.go
+++ b/internal/rpcserver/rpcserverhandlers_test.go
@@ -19,7 +19,9 @@ import (
 	"os"
 	"path/filepath"
 	"reflect"
+	"runtime"
 	"strconv"
+	"strings"
 	"testing"
 	"time"
 
@@ -1189,6 +1191,25 @@ func (mgr *testNtfnManager) Run(ctx context.Context) {
 	<-ctx.Done()
 }
 
+// testHelpCacher provides a mock help and usage cacher by implementing the
+// RPCHelpCacher interface.
+type testHelpCacher struct {
+	methodHelp    string
+	methodHelpErr error
+	usage         string
+	usageErr      error
+}
+
+// RPCMethodHelp returns the mocked RPC help string for the provided method.
+func (c *testHelpCacher) RPCMethodHelp(method types.Method) (string, error) {
+	return c.methodHelp, c.methodHelpErr
+}
+
+// RPCUsage returns the mocked usage for supported RPC commands.
+func (c *testHelpCacher) RPCUsage(includeWebsockets bool) (string, error) {
+	return c.usage, c.usageErr
+}
+
 // mustParseHash converts the passed big-endian hex string into a
 // chainhash.Hash and will panic if there is an error.  It only differs from the
 // one available in chainhash in that it will panic so errors in the source code
@@ -1298,6 +1319,7 @@ type rpcTest struct {
 	mockFiltererV2        *testFiltererV2
 	mockTxMempooler       *testTxMempooler
 	mockMiningAddrs       []dcrutil.Address
+	mockHelpCacher        *testHelpCacher
 	result                interface{}
 	wantErr               bool
 	errCode               dcrjson.RPCErrorCode
@@ -7685,6 +7707,148 @@ func TestHandleGetRawTransaction(t *testing.T) {
 	}})
 }
 
+func TestHandleVersion(t *testing.T) {
+	t.Parallel()
+
+	runtimeVer := strings.Replace(runtime.Version(), ".", "-", -1)
+	version.BuildMetadata = "foo"
+	version.PreRelease = "pre"
+	buildMeta := version.NormalizeBuildString(
+		fmt.Sprintf("%s.%s", version.BuildMetadata, runtimeVer))
+
+	result := map[string]types.VersionResult{
+		"dcrdjsonrpcapi": {
+			VersionString: jsonrpcSemverString,
+			Major:         jsonrpcSemverMajor,
+			Minor:         jsonrpcSemverMinor,
+			Patch:         jsonrpcSemverPatch,
+			Prerelease:    "",
+			BuildMetadata: "",
+		},
+		"dcrd": {
+			VersionString: version.String(),
+			Major:         uint32(version.Major),
+			Minor:         uint32(version.Minor),
+			Patch:         uint32(version.Patch),
+			Prerelease:    version.PreRelease,
+			BuildMetadata: buildMeta,
+		}}
+
+	testRPCServerHandler(t, []rpcTest{{
+		name:    "handleVersion: ok, with build metadata",
+		handler: handleVersion,
+		cmd:     &types.VersionCmd{},
+		wantErr: false,
+		result:  result,
+	}})
+}
+
+func TestHandleGetStakeDifficulty(t *testing.T) {
+	t.Parallel()
+
+	testRPCServerHandler(t, []rpcTest{{
+		name:    "handleGetStakeDifficulty: unable to fetch header by height",
+		handler: handleGetStakeDifficulty,
+		mockChain: func() *testRPCChain {
+			chain := defaultMockRPCChain()
+			chain.headerByHeightErr = errors.New("unable to fetch header " +
+				"by height")
+			return chain
+		}(),
+		cmd:     &types.GetStakeDifficultyCmd{},
+		wantErr: true,
+		errCode: dcrjson.ErrRPCDifficulty,
+	}, {
+		name:    "handleGetStakeDifficulty: ok",
+		handler: handleGetStakeDifficulty,
+		cmd:     &types.GetStakeDifficultyCmd{},
+		result: types.GetStakeDifficultyResult{
+			CurrentStakeDifficulty: 144.2816259,
+			NextStakeDifficulty:    144.2816259,
+		},
+	}})
+}
+
+func TestHandleStop(t *testing.T) {
+	t.Parallel()
+
+	testRPCServerHandler(t, []rpcTest{{
+		name:    "handleStop: ok",
+		handler: handleStop,
+		cmd:     &types.StopCmd{},
+		result:  "dcrd stopping.",
+	}})
+}
+
+func TestHandleHelp(t *testing.T) {
+	t.Parallel()
+
+	unknownCmd := string("nodes")
+	cmd := string("addnode")
+	usage := "command usage"
+	help := "command help"
+
+	testRPCServerHandler(t, []rpcTest{{
+		name:    "handleHelp: unknown command",
+		handler: handleHelp,
+		cmd: &types.HelpCmd{
+			Command: &unknownCmd,
+		},
+		wantErr: true,
+		errCode: dcrjson.ErrRPCInvalidParameter,
+	}, {
+		name:    "handleHelp: unable to generate usage",
+		handler: handleHelp,
+		cmd: &types.HelpCmd{
+			Command: nil,
+		},
+		mockHelpCacher: func() *testHelpCacher {
+			return &testHelpCacher{
+				usageErr: errors.New("unable to generate usage"),
+			}
+		}(),
+		wantErr: true,
+		errCode: dcrjson.ErrRPCInternal.Code,
+	}, {
+		name:    "handleHelp: ok, command is nil",
+		handler: handleHelp,
+		cmd: &types.HelpCmd{
+			Command: nil,
+		},
+		mockHelpCacher: func() *testHelpCacher {
+			return &testHelpCacher{
+				usage: usage,
+			}
+		}(),
+		result: usage,
+	}, {
+		name:    "handleHelp: unable to generate help",
+		handler: handleHelp,
+		cmd: &types.HelpCmd{
+			Command: &cmd,
+		},
+		mockHelpCacher: func() *testHelpCacher {
+			return &testHelpCacher{
+				methodHelpErr: errors.New("unable to generate help"),
+			}
+		}(),
+		wantErr: true,
+		errCode: dcrjson.ErrRPCInternal.Code,
+	}, {
+		name:    "handleHelp: ok",
+		handler: handleHelp,
+		cmd: &types.HelpCmd{
+			Command: &cmd,
+		},
+		mockHelpCacher: func() *testHelpCacher {
+			return &testHelpCacher{
+				methodHelp: help,
+			}
+		}(),
+		result: help,
+	}})
+}
+
 func testRPCServerHandler(t *testing.T, tests []rpcTest) {
 	t.Helper()
 
@@ -7697,6 +7861,7 @@ func testRPCServerHandler(t *testing.T, tests []rpcTest) {
 			// that are provided by the test.
 			chainParams := defaultChainParams
 			workState := newWorkState()
+			helpCacher := &testHelpCacher{}
 			if test.mockChainParams != nil {
 				chainParams = test.mockChainParams
 			}
@@ -7772,11 +7937,15 @@ func testRPCServerHandler(t *testing.T, tests []rpcTest) {
 			if test.mockTxMempooler != nil {
 				rpcserverConfig.TxMempooler = test.mockTxMempooler
 			}
+			if test.mockHelpCacher != nil {
+				helpCacher = test.mockHelpCacher
+			}
 
 			testServer := &Server{
-				cfg:       *rpcserverConfig,
-				ntfnMgr:   new(testNtfnManager),
-				workState: workState,
+				cfg:        *rpcserverConfig,
+				ntfnMgr:    new(testNtfnManager),
+				workState:  workState,
+				helpCacher: helpCacher,
 			}
 			result, err := test.handler(nil, testServer, test.cmd)
 			if test.wantErr {
diff --git a/internal/rpcserver/rpcserverhelp_test.go b/internal/rpcserver/rpcserverhelp_test.go
index b5269741..1079284f 100644
--- a/internal/rpcserver/rpcserverhelp_test.go
+++ b/internal/rpcserver/rpcserverhelp_test.go
@@ -29,33 +29,33 @@ func TestHelp(t *testing.T) {
 
 	// Ensure the usage for every command can be generated without errors.
 	helpCacher := newHelpCacher()
-	if _, err := helpCacher.rpcUsage(true); err != nil {
+	if _, err := helpCacher.RPCUsage(true); err != nil {
 		t.Fatalf("Failed to generate one-line usage: %v", err)
 	}
-	if _, err := helpCacher.rpcUsage(true); err != nil {
+	if _, err := helpCacher.RPCUsage(true); err != nil {
 		t.Fatalf("Failed to generate one-line usage (cached): %v", err)
 	}
 
 	// Ensure the help for every command can be generated without errors.
 	for k := range rpcHandlers {
-		if _, err := helpCacher.rpcMethodHelp(k); err != nil {
+		if _, err := helpCacher.RPCMethodHelp(k); err != nil {
 			t.Errorf("Failed to generate help for method '%v': %v",
 				k, err)
 			continue
 		}
-		if _, err := helpCacher.rpcMethodHelp(k); err != nil {
+		if _, err := helpCacher.RPCMethodHelp(k); err != nil {
 			t.Errorf("Failed to generate help for method '%v'"+
 				"(cached): %v", k, err)
 			continue
 		}
 	}
 	for k := range wsHandlers {
-		if _, err := helpCacher.rpcMethodHelp(k); err != nil {
+		if _, err := helpCacher.RPCMethodHelp(k); err != nil {
 			t.Errorf("Failed to generate help for method '%v': %v",
 				k, err)
 			continue
 		}
-		if _, err := helpCacher.rpcMethodHelp(k); err != nil {
+		if _, err := helpCacher.RPCMethodHelp(k); err != nil {
 			t.Errorf("Failed to generate help for method '%v'"+
 				"(cached): %v", k, err)
 			continue