Repository: decred/dcrd
Pull request #2553 (2021-01-16 14:29:53)

blockchain: Utxo cleanup and test coverage.
This cleans up a few things for `UtxoViewpoint` and `UtxoEntry` and adds some additional test coverage, including:

- blockchain: Use `ReplaceVoteBits` in utxoview tests.
    - This updates the utxo viewpoint tests to use `ReplaceVoteBits` rather than calling `ReplaceVoteBitsN` directly for each vote transaction index within a given block.
- multi: Remove `blockChain` field from `UtxoViewpoint`.
    - This removes the `blockChain` field from the `UtxoViewpoint` struct since it is no longer used.  It additionally removes that field from the `NewUtxoViewpoint` function and updates all callers accordingly.
- blockchain: Move `UtxoEntry` to a separate file.
    - This moves the `UtxoEntry` type and its methods to a separate file in preparation for adding a utxo cache since both utxo views and the utxo cache will hold entries of the `UtxoEntry` type.
- blockchain: Add unit test coverage for `UtxoEntry`. 
    - This adds full unit test coverage for the `UtxoEntry` type and its methods.


Patch:
diff --git a/blockchain/chain.go b/blockchain/chain.go
index 8a4043bd..d1d984b9 100644
--- a/blockchain/chain.go
+++ b/blockchain/chain.go
@@ -1,5 +1,5 @@
 // Copyright (c) 2013-2016 The btcsuite developers
-// Copyright (c) 2015-2020 The Decred developers
+// Copyright (c) 2015-2021 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
@@ -980,7 +980,7 @@ func (b *BlockChain) reorganizeChainInternal(target *blockNode) error {
 	// using that information to unspend all of the spent txos and remove the
 	// utxos created by the blocks.  In addition, if a block votes against its
 	// parent, the regular transactions are reconnected.
-	view := NewUtxoViewpoint(b)
+	view := NewUtxoViewpoint()
 	view.SetBestHash(&tip.hash)
 	var nextBlockToDetach *dcrutil.Block
 	for tip != nil && tip != fork {
@@ -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/headercmt.go b/blockchain/headercmt.go
index 24f7e0c8..123901ef 100644
--- a/blockchain/headercmt.go
+++ b/blockchain/headercmt.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2019-2020 The Decred developers
+// Copyright (c) 2019-2021 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
@@ -74,7 +74,7 @@ func (b *BlockChain) FetchUtxoViewParentTemplate(block *wire.MsgBlock) (*UtxoVie
 	// Since the block template is building on the parent of the current tip,
 	// undo the transactions and spend information for the tip block to reach
 	// the point of view of the block template.
-	view := NewUtxoViewpoint(b)
+	view := NewUtxoViewpoint()
 	view.SetBestHash(&tip.hash)
 	tipBlock, err := b.fetchMainChainBlockByNode(tip)
 	if err != nil {
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/blockchain/utxoentry.go b/blockchain/utxoentry.go
new file mode 100644
index 00000000..98b1a664
--- /dev/null
+++ b/blockchain/utxoentry.go
@@ -0,0 +1,210 @@
+// Copyright (c) 2021 The Decred developers
+// Use of this source code is governed by an ISC
+// license that can be found in the LICENSE file.
+
+package blockchain
+
+import "github.com/decred/dcrd/blockchain/stake/v4"
+
+// utxoFlags defines additional information and state for a transaction output
+// in a utxo view.  The bit representation is:
+//   bit  0    - containing transaction is a coinbase
+//   bit  1    - transaction output has been spent
+//   bit  2    - transaction output has been modified since it was loaded
+//   bit  3    - containing transaction has an expiry
+//   bits 4-7	 - transaction type
+type utxoFlags uint8
+
+const (
+	// utxoFlagCoinBase indicates that a txout was contained in a coinbase tx.
+	utxoFlagCoinBase utxoFlags = 1 << iota
+
+	// utxoFlagSpent indicates that a txout is spent.
+	utxoFlagSpent
+
+	// utxoFlagModified indicates that a txout has been modified since it was
+	// loaded.
+	utxoFlagModified
+
+	// utxoFlagHasExpiry indicates that a txout was contained in a tx that
+	// included an expiry.
+	utxoFlagHasExpiry
+)
+
+const (
+	// utxoFlagTxTypeBitmask describes the bitmask that yields bits 4-7 from
+	// utxoFlags.
+	utxoFlagTxTypeBitmask = 0xf0
+
+	// utxoFlagTxTypeShift is the number of bits to shift utxoFlags to the right
+	// to yield the correct integer value after applying the bitmask with AND.
+	utxoFlagTxTypeShift = 4
+)
+
+// encodeUtxoFlags returns utxoFlags representing the passed parameters.
+func encodeUtxoFlags(coinbase bool, spent bool, modified bool, hasExpiry bool,
+	txType stake.TxType) utxoFlags {
+
+	packedFlags := utxoFlags(txType) << utxoFlagTxTypeShift
+	if coinbase {
+		packedFlags |= utxoFlagCoinBase
+	}
+	if spent {
+		packedFlags |= utxoFlagSpent
+	}
+	if modified {
+		packedFlags |= utxoFlagModified
+	}
+	if hasExpiry {
+		packedFlags |= utxoFlagHasExpiry
+	}
+
+	return packedFlags
+}
+
+// isTicketSubmissionOutput returns true if the output is a ticket submission.
+func isTicketSubmissionOutput(txType stake.TxType, txOutIdx uint32) bool {
+	return txType == stake.TxTypeSStx && txOutIdx == 0
+}
+
+// ticketMinimalOutputs stores the minimal outputs for ticket transactions and
+// is used in ticket submission utxo entries.
+//
+// The minimal outputs for ticket transactions are stored since all outputs of a
+// ticket need to be retrieved when validating vote transaction inputs or
+// revoking a ticket.
+type ticketMinimalOutputs struct {
+	data []byte
+}
+
+// UtxoEntry houses details about an individual transaction output in a utxo
+// view such as whether or not it was contained in a coinbase tx, the height of
+// the block that contains the tx, whether or not it is spent, its public key
+// script, and how much it pays.
+//
+// The struct is aligned for memory efficiency.
+type UtxoEntry struct {
+	amount   int64
+	pkScript []byte
+
+	// ticketMinOuts is the minimal outputs for the ticket transaction that the
+	// output is contained in.  This is only stored in ticket submission outputs
+	// and is nil for all other output types.
+	//
+	// Note that this is using a pointer rather than a slice in order to occupy
+	// less space when it is nil.  It is nil in the vast majority of entries, so
+	// this provides a significant overall reduction in memory usage.
+	ticketMinOuts *ticketMinimalOutputs
+
+	blockHeight   uint32
+	blockIndex    uint32
+	scriptVersion uint16
+
+	// packedFlags contains additional info about the output as defined by
+	// utxoFlags.  This approach is used in order to reduce memory usage since
+	// there will be a lot of these in memory.
+	packedFlags utxoFlags
+}
+
+// isModified returns whether or not the output has been modified since it was
+// loaded.
+func (entry *UtxoEntry) isModified() bool {
+	return entry.packedFlags&utxoFlagModified == utxoFlagModified
+}
+
+// IsCoinBase returns whether or not the output was contained in a coinbase
+// transaction.
+func (entry *UtxoEntry) IsCoinBase() bool {
+	return entry.packedFlags&utxoFlagCoinBase == utxoFlagCoinBase
+}
+
+// IsSpent returns whether or not the output has been spent based upon the
+// current state of the unspent transaction output view it was obtained from.
+func (entry *UtxoEntry) IsSpent() bool {
+	return entry.packedFlags&utxoFlagSpent == utxoFlagSpent
+}
+
+// HasExpiry returns whether or not the output was contained in a transaction
+// that included an expiry.
+func (entry *UtxoEntry) HasExpiry() bool {
+	return entry.packedFlags&utxoFlagHasExpiry == utxoFlagHasExpiry
+}
+
+// BlockHeight returns the height of the block containing the output.
+func (entry *UtxoEntry) BlockHeight() int64 {
+	return int64(entry.blockHeight)
+}
+
+// BlockIndex returns the index of the transaction that the output is contained
+// in.
+func (entry *UtxoEntry) BlockIndex() uint32 {
+	return entry.blockIndex
+}
+
+// TransactionType returns the type of the transaction that the output is
+// contained in.
+func (entry *UtxoEntry) TransactionType() stake.TxType {
+	txType := (entry.packedFlags & utxoFlagTxTypeBitmask) >> utxoFlagTxTypeShift
+	return stake.TxType(txType)
+}
+
+// Spend marks the output as spent.  Spending an output that is already spent
+// has no effect.
+func (entry *UtxoEntry) Spend() {
+	// Nothing to do if the output is already spent.
+	if entry.IsSpent() {
+		return
+	}
+
+	// Mark the output as spent and modified.
+	entry.packedFlags |= utxoFlagSpent | utxoFlagModified
+}
+
+// Amount returns the amount of the output.
+func (entry *UtxoEntry) Amount() int64 {
+	return entry.amount
+}
+
+// PkScript returns the public key script for the output.
+func (entry *UtxoEntry) PkScript() []byte {
+	return entry.pkScript
+}
+
+// ScriptVersion returns the public key script version for the output.
+func (entry *UtxoEntry) ScriptVersion() uint16 {
+	return entry.scriptVersion
+}
+
+// TicketMinimalOutputs returns the minimal outputs for the ticket transaction
+// that the output is contained in.  Note that the ticket minimal outputs are
+// only stored in ticket submission outputs and nil will be returned for all
+// other output types.
+func (entry *UtxoEntry) TicketMinimalOutputs() []*stake.MinimalOutput {
+	if entry.ticketMinOuts == nil {
+		return nil
+	}
+
+	minOuts, _ := deserializeToMinimalOutputs(entry.ticketMinOuts.data)
+	return minOuts
+}
+
+// Clone returns a copy of the utxo entry.  It performs a deep copy for any
+// fields that are mutable.  Specifically, the script and ticket minimal outputs
+// are NOT deep copied since they are immutable.
+func (entry *UtxoEntry) Clone() *UtxoEntry {
+	if entry == nil {
+		return nil
+	}
+
+	newEntry := &UtxoEntry{
+		amount:        entry.amount,
+		pkScript:      entry.pkScript,
+		ticketMinOuts: entry.ticketMinOuts,
+		blockHeight:   entry.blockHeight,
+		blockIndex:    entry.blockIndex,
+		scriptVersion: entry.scriptVersion,
+		packedFlags:   entry.packedFlags,
+	}
+
+	return newEntry
+}
diff --git a/blockchain/utxoviewpoint.go b/blockchain/utxoviewpoint.go
index 9169ded7..caa767c0 100644
--- a/blockchain/utxoviewpoint.go
+++ b/blockchain/utxoviewpoint.go
@@ -1,5 +1,5 @@
 // Copyright (c) 2015-2016 The btcsuite developers
-// Copyright (c) 2015-2020 The Decred developers
+// Copyright (c) 2015-2021 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
@@ -17,208 +17,6 @@ import (
 	"github.com/decred/dcrd/wire"
 )
 
-// utxoFlags defines additional information and state for a transaction output
-// in a utxo view.  The bit representation is:
-//   bit  0    - containing transaction is a coinbase
-//   bit  1    - transaction output has been spent
-//   bit  2    - transaction output has been modified since it was loaded
-//   bit  3    - containing transaction has an expiry
-//   bits 4-7	 - transaction type
-type utxoFlags uint8
-
-const (
-	// utxoFlagCoinBase indicates that a txout was contained in a coinbase tx.
-	utxoFlagCoinBase utxoFlags = 1 << iota
-
-	// utxoFlagSpent indicates that a txout is spent.
-	utxoFlagSpent
-
-	// utxoFlagModified indicates that a txout has been modified since it was
-	// loaded.
-	utxoFlagModified
-
-	// utxoFlagHasExpiry indicates that a txout was contained in a tx that
-	// included an expiry.
-	utxoFlagHasExpiry
-)
-
-const (
-	// utxoFlagTxTypeBitmask describes the bitmask that yields bits 4-7 from
-	// utxoFlags.
-	utxoFlagTxTypeBitmask = 0xf0
-
-	// utxoFlagTxTypeShift is the number of bits to shift utxoFlags to the right
-	// to yield the correct integer value after applying the bitmask with AND.
-	utxoFlagTxTypeShift = 4
-)
-
-// encodeUtxoFlags returns utxoFlags representing the passed parameters.
-func encodeUtxoFlags(coinbase bool, spent bool, modified bool, hasExpiry bool,
-	txType stake.TxType) utxoFlags {
-
-	packedFlags := utxoFlags(txType) << utxoFlagTxTypeShift
-	if coinbase {
-		packedFlags |= utxoFlagCoinBase
-	}
-	if spent {
-		packedFlags |= utxoFlagSpent
-	}
-	if modified {
-		packedFlags |= utxoFlagModified
-	}
-	if hasExpiry {
-		packedFlags |= utxoFlagHasExpiry
-	}
-
-	return packedFlags
-}
-
-// ticketMinimalOutputs stores the minimal outputs for ticket transactions and
-// is used in ticket submission utxo entries.
-//
-// The minimal outputs for ticket transactions are stored since all outputs of a
-// ticket need to be retrieved when validating vote transaction inputs or
-// revoking a ticket.
-type ticketMinimalOutputs struct {
-	data []byte
-}
-
-// UtxoEntry houses details about an individual transaction output in a utxo
-// view such as whether or not it was contained in a coinbase tx, the height of
-// the block that contains the tx, whether or not it is spent, its public key
-// script, and how much it pays.
-//
-// The struct is aligned for memory efficiency.
-type UtxoEntry struct {
-	amount   int64
-	pkScript []byte
-
-	// ticketMinOuts is the minimal outputs for the ticket transaction that the
-	// output is contained in.  This is only stored in ticket submission outputs
-	// and is nil for all other output types.
-	//
-	// Note that this is using a pointer rather than a slice in order to occupy
-	// less space when it is nil.  It is nil in the vast majority of entries, so
-	// this provides a significant overall reduction in memory usage.
-	ticketMinOuts *ticketMinimalOutputs
-
-	blockHeight   uint32
-	blockIndex    uint32
-	scriptVersion uint16
-
-	// packedFlags contains additional info about the output as defined by
-	// utxoFlags.  This approach is used in order to reduce memory usage since
-	// there will be a lot of these in memory.
-	packedFlags utxoFlags
-}
-
-// isModified returns whether or not the output has been modified since it was
-// loaded.
-func (entry *UtxoEntry) isModified() bool {
-	return entry.packedFlags&utxoFlagModified == utxoFlagModified
-}
-
-// IsCoinBase returns whether or not the output was contained in a coinbase
-// transaction.
-func (entry *UtxoEntry) IsCoinBase() bool {
-	return entry.packedFlags&utxoFlagCoinBase == utxoFlagCoinBase
-}
-
-// IsSpent returns whether or not the output has been spent based upon the
-// current state of the unspent transaction output view it was obtained from.
-func (entry *UtxoEntry) IsSpent() bool {
-	return entry.packedFlags&utxoFlagSpent == utxoFlagSpent
-}
-
-// HasExpiry returns whether or not the output was contained in a transaction
-// that included an expiry.
-func (entry *UtxoEntry) HasExpiry() bool {
-	return entry.packedFlags&utxoFlagHasExpiry == utxoFlagHasExpiry
-}
-
-// BlockHeight returns the height of the block containing the output.
-func (entry *UtxoEntry) BlockHeight() int64 {
-	return int64(entry.blockHeight)
-}
-
-// BlockIndex returns the index of the transaction that the output is contained
-// in.
-func (entry *UtxoEntry) BlockIndex() uint32 {
-	return entry.blockIndex
-}
-
-// TransactionType returns the type of the transaction that the output is
-// contained in.
-func (entry *UtxoEntry) TransactionType() stake.TxType {
-	txType := (entry.packedFlags & utxoFlagTxTypeBitmask) >> utxoFlagTxTypeShift
-	return stake.TxType(txType)
-}
-
-// Spend marks the output as spent.  Spending an output that is already spent
-// has no effect.
-func (entry *UtxoEntry) Spend() {
-	// Nothing to do if the output is already spent.
-	if entry.IsSpent() {
-		return
-	}
-
-	// Mark the output as spent and modified.
-	entry.packedFlags |= utxoFlagSpent | utxoFlagModified
-}
-
-// Amount returns the amount of the output.
-func (entry *UtxoEntry) Amount() int64 {
-	return entry.amount
-}
-
-// PkScript returns the public key script for the output.
-func (entry *UtxoEntry) PkScript() []byte {
-	return entry.pkScript
-}
-
-// ScriptVersion returns the public key script version for the output.
-func (entry *UtxoEntry) ScriptVersion() uint16 {
-	return entry.scriptVersion
-}
-
-// TicketMinimalOutputs returns the minimal outputs for the ticket transaction
-// that the output is contained in.  Note that the ticket minimal outputs are
-// only stored in ticket submission outputs and nil will be returned for all
-// other output types.
-func (entry *UtxoEntry) TicketMinimalOutputs() []*stake.MinimalOutput {
-	if entry.ticketMinOuts == nil {
-		return nil
-	}
-
-	minOuts, _ := deserializeToMinimalOutputs(entry.ticketMinOuts.data)
-	return minOuts
-}
-
-// Clone returns a copy of the utxo entry.  It performs a deep copy for any
-// fields that are mutable.
-func (entry *UtxoEntry) Clone() *UtxoEntry {
-	if entry == nil {
-		return nil
-	}
-
-	newEntry := &UtxoEntry{
-		amount:        entry.amount,
-		pkScript:      entry.pkScript,
-		ticketMinOuts: entry.ticketMinOuts,
-		blockHeight:   entry.blockHeight,
-		blockIndex:    entry.blockIndex,
-		scriptVersion: entry.scriptVersion,
-		packedFlags:   entry.packedFlags,
-	}
-
-	return newEntry
-}
-
-// isTicketSubmissionOutput returns true if the output is a ticket submission.
-func isTicketSubmissionOutput(txType stake.TxType, txOutIdx uint32) bool {
-	return txType == stake.TxTypeSStx && txOutIdx == 0
-}
-
 // UtxoViewpoint represents a view into the set of unspent transaction outputs
 // from a specific point of view in the chain.  For example, it could be for
 // the end of the main chain, some point in the history of the main chain, or
@@ -227,9 +25,8 @@ func isTicketSubmissionOutput(txType stake.TxType, txOutIdx uint32) bool {
 // The unspent outputs are needed by other transactions for things such as
 // script validation and double spend prevention.
 type UtxoViewpoint struct {
-	entries    map[wire.OutPoint]*UtxoEntry
-	bestHash   chainhash.Hash
-	blockChain *BlockChain
+	entries  map[wire.OutPoint]*UtxoEntry
+	bestHash chainhash.Hash
 }
 
 // BestHash returns the hash of the best block in the chain the view currently
@@ -958,9 +755,8 @@ func (view *UtxoViewpoint) fetchInputUtxos(db database.DB, block *dcrutil.Block,
 // clone returns a deep copy of the view.
 func (view *UtxoViewpoint) clone() *UtxoViewpoint {
 	clonedView := &UtxoViewpoint{
-		entries:    make(map[wire.OutPoint]*UtxoEntry),
-		bestHash:   view.bestHash,
-		blockChain: view.blockChain,
+		entries:  make(map[wire.OutPoint]*UtxoEntry),
+		bestHash: view.bestHash,
 	}
 
 	for outpoint, entry := range view.entries {
@@ -971,10 +767,9 @@ func (view *UtxoViewpoint) clone() *UtxoViewpoint {
 }
 
 // NewUtxoViewpoint returns a new empty unspent transaction output view.
-func NewUtxoViewpoint(blockChain *BlockChain) *UtxoViewpoint {
+func NewUtxoViewpoint() *UtxoViewpoint {
 	return &UtxoViewpoint{
-		entries:    make(map[wire.OutPoint]*UtxoEntry),
-		blockChain: blockChain,
+		entries: make(map[wire.OutPoint]*UtxoEntry),
 	}
 }
 
@@ -995,7 +790,7 @@ func (b *BlockChain) FetchUtxoView(tx *dcrutil.Tx, includeRegularTxns bool) (*Ut
 	// because the code below requires the parent block and the genesis
 	// block doesn't have one.
 	tip := b.bestChain.Tip()
-	view := NewUtxoViewpoint(b)
+	view := NewUtxoViewpoint()
 	view.SetBestHash(&tip.hash)
 	if tip.height == 0 {
 		return view, nil
diff --git a/blockchain/validate.go b/blockchain/validate.go
index 4a9225bb..ee41de70 100644
--- a/blockchain/validate.go
+++ b/blockchain/validate.go
@@ -1,5 +1,5 @@
 // Copyright (c) 2013-2016 The btcsuite developers
-// Copyright (c) 2015-2020 The Decred developers
+// Copyright (c) 2015-2021 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
@@ -3791,7 +3791,7 @@ func (b *BlockChain) CheckConnectBlockTemplate(block *dcrutil.Block) error {
 			return ruleError(ErrMissingParent, err.Error())
 		}
 
-		view := NewUtxoViewpoint(b)
+		view := NewUtxoViewpoint()
 		view.SetBestHash(&tip.hash)
 
 		return b.checkConnectBlock(newNode, block, parent, view, nil, nil)
@@ -3801,7 +3801,7 @@ func (b *BlockChain) CheckConnectBlockTemplate(block *dcrutil.Block) error {
 	// current tip due to the previous checks, so undo the transactions and
 	// spend information for the tip block to reach the point of view of the
 	// block template.
-	view := NewUtxoViewpoint(b)
+	view := NewUtxoViewpoint()
 	view.SetBestHash(&tip.hash)
 	tipBlock, err := b.fetchMainChainBlockByNode(tip)
 	if err != nil {
diff --git a/cmd/gencerts/gencerts.go b/cmd/gencerts/gencerts.go
index 215f3ffc..c50d71ed 100644
--- a/cmd/gencerts/gencerts.go
+++ b/cmd/gencerts/gencerts.go
@@ -10,6 +10,7 @@ import (
 	"crypto/ed25519"
 	"crypto/elliptic"
 	"crypto/rand"
+	"crypto/rsa"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
@@ -42,7 +43,7 @@ type config struct {
 	Hosts []string `short:"H" description:"hostname or IP certificate is valid for; may be specified multiple times"`
 	Local bool     `short:"L" description:"append localhost, 127.0.0.1, and ::1 to hosts if not already specified"`
 	Org   string   `short:"o" description:"organization"`
-	Algo  string   `short:"a" description:"key algorithm (one of: P-256, P-384, P-521, Ed25519)"`
+	Algo  string   `short:"a" description:"key algorithm (one of: P-256, P-384, P-521, Ed25519, RSA4096)"`
 	Years int      `short:"y" description:"years certificate is valid for"`
 	Force bool     `short:"f" description:"overwrite existing certs/keys"`
 }
@@ -82,6 +83,8 @@ func main() {
 		keygen = ecKeyGen(elliptic.P521())
 	case "Ed25519":
 		keygen = ed25519KeyGen
+	case "RSA4096":
+		keygen = rsaKeyGen(4096)
 	default:
 		fmt.Fprintf(os.Stderr, "unknown algorithm %q\n", cfg.Algo)
 		usage(parser)
@@ -189,6 +192,17 @@ func ecKeyGen(curve elliptic.Curve) func() (pub, priv interface{}) {
 	}
 }
 
+func rsaKeyGen(bits int) func() (pub, priv interface{}) {
+	return func() (pub, priv interface{}) {
+		var key *rsa.PrivateKey
+		key, err := rsa.GenerateKey(rand.Reader, bits)
+		if err != nil {
+			fatalf("generate random RSA key: %v\n", err)
+		}
+		return key.Public(), key
+	}
+}
+
 func randomX509SerialNumber() (*big.Int, error) {
 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
 	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
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..ec49a955 100644
--- a/internal/rpcserver/rpcserver.go
+++ b/internal/rpcserver/rpcserver.go
@@ -2041,17 +2041,21 @@ func handleGetBlockchainInfo(_ context.Context, s *Server, cmd interface{}) (int
 		verifyProgress = math.Min(float64(best.Height)/float64(syncHeight), 1.0)
 	}
 
-	// Fetch the maximum allowed block size.
-	maxBlockSize, err := chain.MaxBlockSize(&best.PrevHash)
-	if err != nil {
-		return nil, rpcInternalError(err.Error(),
-			"Could not fetch max block size.")
+	// Fetch the maximum allowed block size for all blocks other than the
+	// genesis block.
+	params := s.cfg.ChainParams
+	maxBlockSize := int64(params.MaximumBlockSizes[0])
+	if best.PrevHash != zeroHash {
+		maxBlockSize, err = chain.MaxBlockSize(&best.PrevHash)
+		if err != nil {
+			context := "Could not fetch max block size"
+			return nil, rpcInternalError(err.Error(), context)
+		}
 	}
 
 	// Fetch the agendas of the consensus deployments as well as their
 	// threshold states and state activation heights.
 	dInfo := make(map[string]types.AgendaInfo)
-	params := s.cfg.ChainParams
 	defaultStatus := blockchain.ThresholdStateTuple{
 		State: blockchain.ThresholdDefined,
 	}.String()
@@ -3869,7 +3873,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 +3890,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 +5419,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)
diff --git a/server.go b/server.go
index 2ac5b832..6db83815 100644
--- a/server.go
+++ b/server.go
@@ -3581,7 +3581,7 @@ func newServer(ctx context.Context, listenAddrs []string, db database.DB, chainP
 			IsTreasuryAgendaActive:          s.chain.IsTreasuryAgendaActive,
 			MaxTreasuryExpenditure:          s.chain.MaxTreasuryExpenditure,
 			NewUtxoViewpoint: func() *blockchain.UtxoViewpoint {
-				return blockchain.NewUtxoViewpoint(s.chain)
+				return blockchain.NewUtxoViewpoint()
 			},
 			TipGeneration: s.chain.TipGeneration,
 			ValidateTransactionScripts: func(tx *dcrutil.Tx,


Test patch:
diff --git a/blockchain/sequencelock_test.go b/blockchain/sequencelock_test.go
index 4d74b662..8746fe2a 100644
--- a/blockchain/sequencelock_test.go
+++ b/blockchain/sequencelock_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2017-2020 The Decred developers
+// Copyright (c) 2017-2021 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
@@ -55,7 +55,7 @@ func TestCalcSequenceLock(t *testing.T) {
 			PkScript: nil,
 		}},
 	})
-	view := NewUtxoViewpoint(bc)
+	view := NewUtxoViewpoint()
 	view.AddTxOuts(targetTx, int64(numBlocks)-4, 0, noTreasury)
 	view.SetBestHash(&node.hash)
 
diff --git a/blockchain/utxoentry_test.go b/blockchain/utxoentry_test.go
new file mode 100644
index 00000000..2184d9d8
--- /dev/null
+++ b/blockchain/utxoentry_test.go
@@ -0,0 +1,287 @@
+// Copyright (c) 2021 The Decred developers
+// Use of this source code is governed by an ISC
+// license that can be found in the LICENSE file.
+
+package blockchain
+
+import (
+	"bytes"
+	"reflect"
+	"testing"
+
+	"github.com/decred/dcrd/blockchain/stake/v4"
+)
+
+// TestEncodeUtxoFlags validates that the correct bit representation is returned
+// for various combinations of flags.
+func TestEncodeUtxoFlags(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name      string
+		coinbase  bool
+		spent     bool
+		modified  bool
+		hasExpiry bool
+		txType    stake.TxType
+		want      utxoFlags
+	}{{
+		name:      "no flags set, regular tx",
+		coinbase:  false,
+		spent:     false,
+		modified:  false,
+		hasExpiry: false,
+		txType:    stake.TxTypeRegular,
+		want:      0x00,
+	}, {
+		name:      "coinbase, has expiry, vote tx",
+		coinbase:  true,
+		spent:     false,
+		modified:  false,
+		hasExpiry: true,
+		txType:    stake.TxTypeSSGen,
+		want:      0x29,
+	}, {
+		name:      "spent, modified, has expiry, ticket tx",
+		coinbase:  false,
+		spent:     true,
+		modified:  true,
+		hasExpiry: true,
+		txType:    stake.TxTypeSStx,
+		want:      0x1e,
+	}}
+
+	for _, test := range tests {
+		got := encodeUtxoFlags(test.coinbase, test.spent, test.modified,
+			test.hasExpiry, test.txType)
+		if got != test.want {
+			t.Errorf("%q: unexpected result -- got %x, want %x", test.name, got,
+				test.want)
+		}
+	}
+}
+
+// TestIsTicketSubmissionOutput validates that the determination of whether
+// various inputs represent a ticket submission output or not is correct.
+func TestIsTicketSubmissionOutput(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		txType   stake.TxType
+		txOutIdx uint32
+		want     bool
+	}{{
+		name:     "ticket tx, output 0",
+		txType:   stake.TxTypeSStx,
+		txOutIdx: 0,
+		want:     true,
+	}, {
+		name:     "ticket tx, output 1",
+		txType:   stake.TxTypeSStx,
+		txOutIdx: 1,
+		want:     false,
+	}, {
+		name:     "vote tx, output 0",
+		txType:   stake.TxTypeSSGen,
+		txOutIdx: 0,
+		want:     false,
+	}}
+
+	for _, test := range tests {
+		got := isTicketSubmissionOutput(test.txType, test.txOutIdx)
+		if got != test.want {
+			t.Errorf("%q: unexpected result -- got %v, want %v", test.name, got,
+				test.want)
+		}
+	}
+}
+
+// TestUtxoEntry validates that the utxo entry methods behave as expected under
+// a variety of conditions.
+func TestUtxoEntry(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name                      string
+		coinbase                  bool
+		spent                     bool
+		modified                  bool
+		expiry                    bool
+		txType                    stake.TxType
+		amount                    int64
+		pkScript                  []byte
+		blockHeight               uint32
+		blockIndex                uint32
+		scriptVersion             uint16
+		ticketMinOuts             *ticketMinimalOutputs
+		deserializedTicketMinOuts []*stake.MinimalOutput
+	}{{
+		name:     "coinbase output",
+		coinbase: true,
+		txType:   stake.TxTypeRegular,
+		amount:   9999,
+		pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688a" +
+			"c"),
+		blockHeight:   54321,
+		blockIndex:    0,
+		scriptVersion: 0,
+	}, {
+		name:   "ticket submission output",
+		expiry: true,
+		txType: stake.TxTypeSStx,
+		amount: 4294959555,
+		pkScript: hexToBytes("ba76a914a13afb81d54c9f8bb0c5e082d56fd563ab9b359688a" +
+			"c"),
+		blockHeight:   55555,
+		blockIndex:    1,
+		scriptVersion: 0,
+		ticketMinOuts: &ticketMinimalOutputs{
+			data: hexToBytes("03808efefade57001aba76a914a13afb81d54c9f8bb0c5e082d56" +
+				"fd563ab9b359688ac0000206a1e9ac39159847e259c9162405b5f6c8135d2c7eaf1a" +
+				"375040001000000005800001abd76a91400000000000000000000000000000000000" +
+				"0000088ac"),
+		},
+		deserializedTicketMinOuts: []*stake.MinimalOutput{{
+			PkScript: hexToBytes("ba76a914a13afb81d54c9f8bb0c5e082d56fd563ab9b35968" +
+				"8ac"),
+			Value:   4294959555,
+			Version: 0,
+		}, {
+			PkScript: hexToBytes("6a1e9ac39159847e259c9162405b5f6c8135d2c7eaf1a3750" +
+				"400010000000058"),
+			Value:   0,
+			Version: 0,
+		}, {
+			PkScript: hexToBytes("bd76a91400000000000000000000000000000000000000008" +
+				"8ac"),
+			Value:   0,
+			Version: 0,
+		}},
+	}}
+
+	for _, test := range tests {
+		// Create an entry given the parameters for the current test.
+		entry := &UtxoEntry{
+			amount:        test.amount,
+			pkScript:      test.pkScript,
+			ticketMinOuts: test.ticketMinOuts,
+			blockHeight:   test.blockHeight,
+			blockIndex:    test.blockIndex,
+			scriptVersion: test.scriptVersion,
+			packedFlags: encodeUtxoFlags(
+				test.coinbase,
+				test.spent,
+				test.modified,
+				test.expiry,
+				test.txType,
+			),
+		}
+
+		// Validate the modified flag.
+		isModified := entry.isModified()
+		if isModified != test.modified {
+			t.Fatalf("unexpected modified flag -- got %v, want %v", isModified,
+				test.modified)
+		}
+
+		// Validate the coinbase flag.
+		isCoinBase := entry.IsCoinBase()
+		if isCoinBase != test.coinbase {
+			t.Fatalf("unexpected coinbase flag -- got %v, want %v", isCoinBase,
+				test.coinbase)
+		}
+
+		// Validate the spent flag.
+		isSpent := entry.IsSpent()
+		if isSpent != test.spent {
+			t.Fatalf("unexpected spent flag -- got %v, want %v", isSpent, test.spent)
+		}
+
+		// Validate the expiry flag.
+		hasExpiry := entry.HasExpiry()
+		if hasExpiry != test.expiry {
+			t.Fatalf("unexpected expiry flag -- got %v, want %v", hasExpiry,
+				test.expiry)
+		}
+
+		// Validate the height of the block containing the output.
+		gotBlockHeight := entry.BlockHeight()
+		if gotBlockHeight != int64(test.blockHeight) {
+			t.Fatalf("unexpected block height -- got %v, want %v", gotBlockHeight,
+				int64(test.blockHeight))
+		}
+
+		// Validate the index of the transaction that the output is contained in.
+		gotBlockIndex := entry.BlockIndex()
+		if gotBlockIndex != test.blockIndex {
+			t.Fatalf("unexpected block index -- got %v, want %v", gotBlockIndex,
+				test.blockIndex)
+		}
+
+		// Validate the type of the transaction that the output is contained in.
+		gotTxType := entry.TransactionType()
+		if gotTxType != test.txType {
+			t.Fatalf("unexpected transaction type -- got %v, want %v", gotTxType,
+				test.txType)
+		}
+
+		// Validate the amount of the output.
+		gotAmount := entry.Amount()
+		if gotAmount != test.amount {
+			t.Fatalf("unexpected amount -- got %v, want %v", gotAmount, test.amount)
+		}
+
+		// Validate the script of the output.
+		gotScript := entry.PkScript()
+		if !bytes.Equal(gotScript, test.pkScript) {
+			t.Fatalf("unexpected script -- got %v, want %v", gotScript, test.pkScript)
+		}
+
+		// Validate the script version of the output.
+		gotScriptVersion := entry.ScriptVersion()
+		if gotScriptVersion != test.scriptVersion {
+			t.Fatalf("unexpected script version -- got %v, want %v", gotScriptVersion,
+				test.scriptVersion)
+		}
+
+		// Spend the entry.  Validate that it is marked as spent and modified.
+		entry.Spend()
+		if !entry.IsSpent() {
+			t.Fatal("expected entry to be spent")
+		}
+		if !entry.isModified() {
+			t.Fatal("expected entry to be modified")
+		}
+
+		// Validate that if spend is called again the entry is still marked as spent
+		// and modified.
+		entry.Spend()
+		if !entry.IsSpent() {
+			t.Fatal("expected entry to still be marked as spent")
+		}
+		if !entry.isModified() {
+			t.Fatal("expected entry to still be marked as modified")
+		}
+
+		// Validate the ticket minimal outputs.
+		ticketMinOutsResult := entry.TicketMinimalOutputs()
+		if !reflect.DeepEqual(ticketMinOutsResult, test.deserializedTicketMinOuts) {
+			t.Fatalf("unexpected ticket min outs -- got %v, want %v",
+				ticketMinOutsResult, test.deserializedTicketMinOuts)
+		}
+
+		// Clone the entry and validate that all values are deep equal.
+		clonedEntry := entry.Clone()
+		if !reflect.DeepEqual(clonedEntry, entry) {
+			t.Fatalf("expected entry to be equal to cloned entry -- got %v, want %v",
+				clonedEntry, entry)
+		}
+
+		// Validate that clone returns nil when called on a nil entry.
+		var nilEntry *UtxoEntry
+		if nilEntry.Clone() != nil {
+			t.Fatal("expected nil when calling clone on a nil entry")
+		}
+	}
+}
diff --git a/blockchain/utxoviewpoint_test.go b/blockchain/utxoviewpoint_test.go
index 2d93c678..0bfb4572 100644
--- a/blockchain/utxoviewpoint_test.go
+++ b/blockchain/utxoviewpoint_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 The Decred developers
+// Copyright (c) 2020-2021 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
@@ -118,9 +118,7 @@ func TestFetchUtxoView(t *testing.T) {
 	outs = g.OldestCoinbaseOuts()
 	g.NextBlock("b1", &outs[0], outs[1:], func(b *wire.MsgBlock) {
 		b.Header.VoteBits &^= voteBitYes
-		for i := 0; i < 5; i++ {
-			g.ReplaceVoteBitsN(i, voteBitNo)(b)
-		}
+		g.ReplaceVoteBits(voteBitNo)(b)
 	})
 	g.AssertTipDisapprovesPrevious()
 	g.AcceptTipBlock()
@@ -174,9 +172,7 @@ func TestFetchUtxoView(t *testing.T) {
 	g.SetTip("b0")
 	b1a := g.NextBlock("b1a", &outs[0], outs[1:], func(b *wire.MsgBlock) {
 		b.Header.VoteBits &^= voteBitYes
-		for i := 0; i < 5; i++ {
-			g.ReplaceVoteBitsN(i, voteBitNo)(b)
-		}
+		g.ReplaceVoteBits(voteBitNo)(b)
 
 		b.Transactions[1] = b0Tx1.MsgTx()
 	})
diff --git a/internal/mempool/mempool_test.go b/internal/mempool/mempool_test.go
index 9546d1cf..a1ef4ace 100644
--- a/internal/mempool/mempool_test.go
+++ b/internal/mempool/mempool_test.go
@@ -1,5 +1,5 @@
 // Copyright (c) 2016 The btcsuite developers
-// Copyright (c) 2017-2020 The Decred developers
+// Copyright (c) 2017-2021 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
@@ -87,7 +87,7 @@ func (s *fakeChain) FetchUtxoView(tx *dcrutil.Tx, treeValid bool) (*blockchain.U
 
 	// Add entries for the outputs of the tx to the new view.
 	msgTx := tx.MsgTx()
-	viewpoint := blockchain.NewUtxoViewpoint(nil)
+	viewpoint := blockchain.NewUtxoViewpoint()
 	outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tx.Tree()}
 	for txOutIdx := range msgTx.TxOut {
 		outpoint.Index = uint32(txOutIdx)
@@ -754,7 +754,7 @@ func newPoolHarness(chainParams *chaincfg.Params) (*poolHarness, []spendableOutp
 	// Create a new fake chain and harness bound to it.
 	subsidyCache := standalone.NewSubsidyCache(chainParams)
 	chain := &fakeChain{
-		utxos:       blockchain.NewUtxoViewpoint(nil),
+		utxos:       blockchain.NewUtxoViewpoint(),
 		utxoTimes:   make(map[wire.OutPoint]int64),
 		blocks:      make(map[chainhash.Hash]*dcrutil.Block),
 		scriptFlags: BaseStandardVerifyFlags,
diff --git a/internal/mining/mining_harness_test.go b/internal/mining/mining_harness_test.go
index ba70f603..f95cc1f9 100644
--- a/internal/mining/mining_harness_test.go
+++ b/internal/mining/mining_harness_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2020 The Decred developers
+// Copyright (c) 2020-2021 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
@@ -144,7 +144,7 @@ func (c *fakeChain) FetchUtxoView(tx *dcrutil.Tx, treeValid bool) (*blockchain.U
 
 	// Add entries for the outputs of the tx to the new view.
 	msgTx := tx.MsgTx()
-	viewpoint := blockchain.NewUtxoViewpoint(nil)
+	viewpoint := blockchain.NewUtxoViewpoint()
 	prevOut := wire.OutPoint{Hash: *tx.Hash(), Tree: tx.Tree()}
 	for txOutIdx := range msgTx.TxOut {
 		prevOut.Index = uint32(txOutIdx)
@@ -195,7 +195,7 @@ func (c *fakeChain) MaxTreasuryExpenditure(preTVIBlock *chainhash.Hash) (int64,
 
 // NewUtxoViewpoint returns a new empty unspent transaction output view.
 func (c *fakeChain) NewUtxoViewpoint() *blockchain.UtxoViewpoint {
-	return blockchain.NewUtxoViewpoint(nil)
+	return blockchain.NewUtxoViewpoint()
 }
 
 // TipGeneration returns a mocked entire generation of blocks stemming from the
@@ -1296,8 +1296,8 @@ func newMiningHarness(chainParams *chaincfg.Params) (*miningHarness, []spendable
 		blocks:                          make(map[chainhash.Hash]*dcrutil.Block),
 		isHeaderCommitmentsAgendaActive: true,
 		isTreasuryAgendaActive:          true,
-		parentUtxos:                     blockchain.NewUtxoViewpoint(nil),
-		utxos:                           blockchain.NewUtxoViewpoint(nil),
+		parentUtxos:                     blockchain.NewUtxoViewpoint(),
+		utxos:                           blockchain.NewUtxoViewpoint(),
 	}
 
 	// Set the proof of work limit and next required difficulty very high by
diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go
index 5cbe276a..eedb74a9 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
@@ -3584,7 +3606,8 @@ func TestHandleGetBlockchainInfo(t *testing.T) {
 		mockChain: func() *testRPCChain {
 			chain := defaultMockRPCChain()
 			chain.bestSnapshot = &blockchain.BestState{
-				Hash: *hash,
+				Hash:     *hash,
+				PrevHash: *prevHash,
 			}
 			chain.chainWork = big.NewInt(0).SetBytes([]byte{0x11, 0x5d, 0x28, 0x33, 0x84,
 				0x90, 0x90, 0xb0, 0x02, 0x65, 0x06})
@@ -7685,6 +7708,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 +7862,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 +7938,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