Repository: decred/dcrd
Pull request #1829 (2019-08-09 20:59:32)

mempool/mining: Implement aggregate fee sorting.
Relates to #1556 

This pull request modified the mining prioritization logic to sort transactions by an aggregate fee based on a transaction’s ancestors in the mempool.

Currently, the mining code prioritizes transactions by fee and only includes them in the block template if they have no dependencies. When a transaction is included in the block template, it is removed as a dependency from transactions that depend on it, making those dependent transactions eligible for block template inclusion.

Aggregating ancestor statistics for large graphs of transactions in the mempool may have high computational costs. Performing this aggregation in the block template generator would delay miners requesting a block template since all of the aggregation would need to occur for each transaction, at once. To solve this problem, the complexity is spread over time such that the transaction statistics are updated with mempool event hooks. Since the mempool is not locked during the entirety of template generation, a mining view interface is exposed to safely interact with a snapshot of the mempool.

The biggest risk in terms of performance are reorgs where transactions with many descendants are added back to the mempool. The number of transactions added to the mempool in this way is limited by the block size and proof of work, but the mempool has no restrictions on the number of ancestors tracked for a given transaction.

A potential - and not implemented - solution to this reorg problem would be to establish a limit on the number of ancestors a transaction may have in the mempool.

davecgh: Nice!  This will obviously need a lot of review and testing, but the idea is sound and I'm happy to see work towards this.
davecgh: Hit a panic while testing this prior to review:
```
panic: interface conversion: interface {} is *mining.TxDesc, not *main.txPrioItem

goroutine 445 [running]:
main.(*txPriorityQueue).Push(0xc013acf980, 0xa6e500, 0xc012f668c0)
        src/github.com/decred/dcrd/mining.go:118 +0x100
container/heap.Push(0xde6760, 0xc013acf980, 0xa6e500, 0xc012f668c0)
        D:/go/src/container/heap/heap.go:53 +0x4c
main.(*BlkTmplGenerator).NewBlockTemplate(0xc012f66b40, 0xde44a0, 0xc00001a7e0, 0xd14eb0, 0xc0151fbf60, 0x42fd06)
        src/github.com/decred/dcrd/mining.go:1513 +0x222b
main.(*BgBlkTmplGenerator).genTemplateAsync.func1(0xc00b5c20c0, 0xde40e0, 0xc014c02340, 0x2, 0xc0004c6300)
        src/github.com/decred/dcrd/mining.go:2562 +0xac
created by main.(*BgBlkTmplGenerator).genTemplateAsync
        src/github.com/decred/dcrd/mining.go:2554 +0xff
```
dnldd: Tested this PR with dcrpool's mining harness, I'm yet to identify the root cause of the issue but I can't mine past SVH because tickets are not being bought. Switching to master fixes this issue, will take a closer look.
davecgh: Also, please rebase this to the latest master.  I would like to test it against the latest code and it currently has merge conflicts.
sefbkn: > Also, please rebase this to the latest master. I would like to test it against the latest code and it currently has merge conflicts.

This has been rebased onto master.
sefbkn: Given the size of this PR and the difficulty of exercising the test cases around the paths around ancestor aggregation during template gen, I think trimming this PR back to an earlier point (last week) would be best at this point:

* Full mining view implementation in the mempool w/ tests.
* Mining prioritization uses `TxMiningView.Children()` to prioritize transactions without dependencies.
* Remove the usage of ancestors during block template gen.

Plus 
* Incorporating the rest of the feedback around the mining view and tests.
* Rebase + squash when needed to clear up commits.

And the major changes to the template gen should get split out into another PR and addressed at a later time. Does this seem fair? 

I'm really not clear on what minimum functionality is or has been expected to get this PR merged since there's been little chatter on code that's sitting here for almost a year, until now.
davecgh: I'll be giving this a final review and doing some more testing later today, but based on the updates, my previous review, and the fact I have been running the code without issue (with template generation active so it is being exercised of course), I believe this is probably pretty close to ready to go in.

To answer your question, the key points for me to merge this now are:

* It works as intended, perhaps most importantly, including when given unexpected data
* It does not introduce any new vulnerabilities or horrible performance regressions


Patch:
diff --git a/blockchain/chain.go b/blockchain/chain.go
index 477e7ad8..3c157851 100644
--- a/blockchain/chain.go
+++ b/blockchain/chain.go
@@ -1260,21 +1260,22 @@ func (b *BlockChain) forceHeadReorganization(formerBest chainhash.Hash, newBest
 	// We can't reorganize the chain unless our head block matches up with
 	// b.bestChain.
 	if !formerBestNode.hash.IsEqual(&formerBest) {
-		return ruleError(ErrForceReorgWrongChain, "tried to force reorg "+
-			"on wrong chain")
+		str := "tried to force reorg on wrong chain"
+		return ruleError(ErrForceReorgWrongChain, str)
 	}
 
 	// Child to reorganize to is missing.
 	newBestNode := b.index.LookupNode(&newBest)
 	if newBestNode == nil || newBestNode.parent != formerBestNode.parent {
-		return ruleError(ErrForceReorgMissingChild, "missing child of "+
-			"common parent for forced reorg")
+		str := "missing child of common parent for forced reorg"
+		return ruleError(ErrForceReorgMissingChild, str)
 	}
 
 	// Don't allow a reorganize to a known invalid chain.
 	newBestNodeStatus := b.index.NodeStatus(newBestNode)
 	if newBestNodeStatus.KnownInvalid() {
-		return ruleError(ErrKnownInvalidBlock, "block is known to be invalid")
+		str := "block is known to be invalid"
+		return ruleError(ErrKnownInvalidBlock, str)
 	}
 
 	// Reorganize the chain and flush any potential unsaved changes to the
diff --git a/blockchain/error.go b/blockchain/error.go
index 5edce24a..dd2de481 100644
--- a/blockchain/error.go
+++ b/blockchain/error.go
@@ -6,7 +6,6 @@
 package blockchain
 
 import (
-	"errors"
 	"fmt"
 
 	"github.com/decred/dcrd/chaincfg/chainhash"
@@ -93,703 +92,563 @@ func (e AssertError) Error() string {
 	return "assertion failed: " + string(e)
 }
 
-// ErrorCode identifies a kind of error.
-type ErrorCode int
+// ErrorKind identifies a kind of error.  It has full support for errors.Is and
+// errors.As, so the caller can directly check against an error kind when
+// determining the reason for an error.
+type ErrorKind string
 
 // These constants are used to identify a specific RuleError.
 const (
 	// ErrDuplicateBlock indicates a block with the same hash already
 	// exists.
-	ErrDuplicateBlock ErrorCode = iota
+	ErrDuplicateBlock = ErrorKind("ErrDuplicateBlock")
 
 	// ErrMissingParent indicates that the block was an orphan.
-	ErrMissingParent
+	ErrMissingParent = ErrorKind("ErrMissingParent")
 
 	// ErrBlockTooBig indicates the serialized block size exceeds the
 	// maximum allowed size.
-	ErrBlockTooBig
+	ErrBlockTooBig = ErrorKind("ErrBlockTooBig")
 
 	// ErrWrongBlockSize indicates that the block size in the header is not
 	// the actual serialized size of the block.
-	ErrWrongBlockSize
+	ErrWrongBlockSize = ErrorKind("ErrWrongBlockSize")
 
 	// ErrBlockVersionTooOld indicates the block version is too old and is
 	// no longer accepted since the majority of the network has upgraded
 	// to a newer version.
-	ErrBlockVersionTooOld
+	ErrBlockVersionTooOld = ErrorKind("ErrBlockVersionTooOld")
 
 	// ErrBadStakeVersionindicates the block version is too old and is no
 	// longer accepted since the majority of the network has upgraded to a
 	// newer version.
-	ErrBadStakeVersion
+	ErrBadStakeVersion = ErrorKind("ErrBadStakeVersion")
 
 	// ErrInvalidTime indicates the time in the passed block has a precision
 	// that is more than one second.  The chain consensus rules require
 	// timestamps to have a maximum precision of one second.
-	ErrInvalidTime
+	ErrInvalidTime = ErrorKind("ErrInvalidTime")
 
 	// ErrTimeTooOld indicates the time is either before the median time of
 	// the last several blocks per the chain consensus rules or prior to the
 	// most recent checkpoint.
-	ErrTimeTooOld
+	ErrTimeTooOld = ErrorKind("ErrTimeTooOld")
 
 	// ErrTimeTooNew indicates the time is too far in the future as compared
 	// the current time.
-	ErrTimeTooNew
+	ErrTimeTooNew = ErrorKind("ErrTimeTooNew")
 
 	// ErrDifficultyTooLow indicates the difficulty for the block is lower
 	// than the difficulty required by the most recent checkpoint.
-	ErrDifficultyTooLow
+	ErrDifficultyTooLow = ErrorKind("ErrDifficultyTooLow")
 
 	// ErrUnexpectedDifficulty indicates specified bits do not align with
 	// the expected value either because it doesn't match the calculated
 	// value based on difficulty regarding the rules or it is out of the
 	// valid range.
-	ErrUnexpectedDifficulty
+	ErrUnexpectedDifficulty = ErrorKind("ErrUnexpectedDifficulty")
 
 	// ErrHighHash indicates the block does not hash to a value which is
 	// lower than the required target difficultly.
-	ErrHighHash
+	ErrHighHash = ErrorKind("ErrHighHash")
 
 	// ErrBadMerkleRoot indicates the calculated merkle root does not match
 	// the expected value.
-	ErrBadMerkleRoot
+	ErrBadMerkleRoot = ErrorKind("ErrBadMerkleRoot")
 
 	// ErrBadCommitmentRoot indicates the calculated commitment root does
 	// not match the expected value.
-	ErrBadCommitmentRoot
+	ErrBadCommitmentRoot = ErrorKind("ErrBadCommitmentRoot")
 
 	// ErrBadCheckpoint indicates a block that is expected to be at a
 	// checkpoint height does not match the expected one.
-	ErrBadCheckpoint
+	ErrBadCheckpoint = ErrorKind("ErrBadCheckpoint")
 
 	// ErrForkTooOld indicates a block is attempting to fork the block chain
 	// before the most recent checkpoint.
-	ErrForkTooOld
+	ErrForkTooOld = ErrorKind("ErrForkTooOld")
 
 	// ErrCheckpointTimeTooOld indicates a block has a timestamp before the
 	// most recent checkpoint.
-	ErrCheckpointTimeTooOld
+	ErrCheckpointTimeTooOld = ErrorKind("ErrCheckpointTimeTooOld")
 
 	// ErrNoTransactions indicates the block does not have a least one
 	// transaction.  A valid block must have at least the coinbase
 	// transaction.
-	ErrNoTransactions
+	ErrNoTransactions = ErrorKind("ErrNoTransactions")
 
 	// ErrTooManyTransactions indicates the block has more transactions than
 	// are allowed.
-	ErrTooManyTransactions
+	ErrTooManyTransactions = ErrorKind("ErrTooManyTransactions")
 
 	// ErrNoTxInputs indicates a transaction does not have any inputs.  A
 	// valid transaction must have at least one input.
-	ErrNoTxInputs
+	ErrNoTxInputs = ErrorKind("ErrNoTxInputs")
 
 	// ErrNoTxOutputs indicates a transaction does not have any outputs.  A
 	// valid transaction must have at least one output.
-	ErrNoTxOutputs
+	ErrNoTxOutputs = ErrorKind("ErrNoTxOutputs")
 
 	// ErrInvalidTxOutputs indicates a transaction does not have the exact
 	// number of outputs.
-	ErrInvalidTxOutputs
+	ErrInvalidTxOutputs = ErrorKind("ErrInvalidTxOutputs")
 
 	// ErrTxTooBig indicates a transaction exceeds the maximum allowed size
 	// when serialized.
-	ErrTxTooBig
+	ErrTxTooBig = ErrorKind("ErrTxTooBig")
 
 	// ErrBadTxOutValue indicates an output value for a transaction is
 	// invalid in some way such as being out of range.
-	ErrBadTxOutValue
+	ErrBadTxOutValue = ErrorKind("ErrBadTxOutValue")
 
 	// ErrDuplicateTxInputs indicates a transaction references the same
 	// input more than once.
-	ErrDuplicateTxInputs
+	ErrDuplicateTxInputs = ErrorKind("ErrDuplicateTxInputs")
 
 	// ErrBadTxInput indicates a transaction input is invalid in some way
 	// such as referencing a previous transaction outpoint which is out of
 	// range or not referencing one at all.
-	ErrBadTxInput
+	ErrBadTxInput = ErrorKind("ErrBadTxInput")
 
 	// ErrMissingTxOut indicates a transaction output referenced by an input
 	// either does not exist or has already been spent.
-	ErrMissingTxOut
+	ErrMissingTxOut = ErrorKind("ErrMissingTxOut")
 
 	// ErrUnfinalizedTx indicates a transaction has not been finalized.
 	// A valid block may only contain finalized transactions.
-	ErrUnfinalizedTx
+	ErrUnfinalizedTx = ErrorKind("ErrUnfinalizedTx")
 
 	// ErrDuplicateTx indicates a block contains an identical transaction
 	// (or at least two transactions which hash to the same value).  A
 	// valid block may only contain unique transactions.
-	ErrDuplicateTx
+	ErrDuplicateTx = ErrorKind("ErrDuplicateTx")
 
 	// ErrOverwriteTx indicates a block contains a transaction that has
 	// the same hash as a previous transaction which has not been fully
 	// spent.
-	ErrOverwriteTx
+	ErrOverwriteTx = ErrorKind("ErrOverwriteTx")
 
 	// ErrImmatureSpend indicates a transaction is attempting to spend a
 	// coinbase that has not yet reached the required maturity.
-	ErrImmatureSpend
+	ErrImmatureSpend = ErrorKind("ErrImmatureSpend")
 
 	// ErrSpendTooHigh indicates a transaction is attempting to spend more
 	// value than the sum of all of its inputs.
-	ErrSpendTooHigh
+	ErrSpendTooHigh = ErrorKind("ErrSpendTooHigh")
 
 	// ErrBadFees indicates the total fees for a block are invalid due to
 	// exceeding the maximum possible value.
-	ErrBadFees
+	ErrBadFees = ErrorKind("ErrBadFees")
 
 	// ErrTooManySigOps indicates the total number of signature operations
 	// for a transaction or block exceed the maximum allowed limits.
-	ErrTooManySigOps
+	ErrTooManySigOps = ErrorKind("ErrTooManySigOps")
 
 	// ErrFirstTxNotCoinbase indicates the first transaction in a block
 	// is not a coinbase transaction.
-	ErrFirstTxNotCoinbase
+	ErrFirstTxNotCoinbase = ErrorKind("ErrFirstTxNotCoinbase")
 
 	// ErrFirstTxNotOpReturn indicates the first transaction in a block
 	// is not an OP_RETURN.
-	ErrFirstTxNotOpReturn
+	ErrFirstTxNotOpReturn = ErrorKind("ErrFirstTxNotOpReturn")
 
 	// ErrCoinbaseHeight indicates that the encoded height in the coinbase
 	// is incorrect.
-	ErrCoinbaseHeight
+	ErrCoinbaseHeight = ErrorKind("ErrCoinbaseHeight")
 
 	// ErrMultipleCoinbases indicates a block contains more than one
 	// coinbase transaction.
-	ErrMultipleCoinbases
+	ErrMultipleCoinbases = ErrorKind("ErrMultipleCoinbases")
 
 	// ErrStakeTxInRegularTree indicates a stake transaction was found in
 	// the regular transaction tree.
-	ErrStakeTxInRegularTree
+	ErrStakeTxInRegularTree = ErrorKind("ErrStakeTxInRegularTree")
 
 	// ErrRegTxInStakeTree indicates that a regular transaction was found in
 	// the stake transaction tree.
-	ErrRegTxInStakeTree
+	ErrRegTxInStakeTree = ErrorKind("ErrRegTxInStakeTree")
 
 	// ErrBadCoinbaseScriptLen indicates the length of the signature script
 	// for a coinbase transaction is not within the valid range.
-	ErrBadCoinbaseScriptLen
+	ErrBadCoinbaseScriptLen = ErrorKind("ErrBadCoinbaseScriptLen")
 
 	// ErrBadCoinbaseValue indicates the amount of a coinbase value does
 	// not match the expected value of the subsidy plus the sum of all fees.
-	ErrBadCoinbaseValue
+	ErrBadCoinbaseValue = ErrorKind("ErrBadCoinbaseValue")
 
 	// ErrBadCoinbaseOutpoint indicates that the outpoint used by a coinbase
 	// as input was non-null.
-	ErrBadCoinbaseOutpoint
+	ErrBadCoinbaseOutpoint = ErrorKind("ErrBadCoinbaseOutpoint")
 
 	// ErrBadCoinbaseFraudProof indicates that the fraud proof for a coinbase
 	// input was non-null.
-	ErrBadCoinbaseFraudProof
+	ErrBadCoinbaseFraudProof = ErrorKind("ErrBadCoinbaseFraudProof")
 
 	// ErrBadCoinbaseAmountIn indicates that the AmountIn (=subsidy) for a
 	// coinbase input was incorrect.
-	ErrBadCoinbaseAmountIn
+	ErrBadCoinbaseAmountIn = ErrorKind("ErrBadCoinbaseAmountIn")
 
 	// ErrBadStakebaseAmountIn indicates that the AmountIn (=subsidy) for a
 	// stakebase input was incorrect.
-	ErrBadStakebaseAmountIn
+	ErrBadStakebaseAmountIn = ErrorKind("ErrBadStakebaseAmountIn")
 
 	// ErrBadStakebaseScriptLen indicates the length of the signature script
 	// for a stakebase transaction is not within the valid range.
-	ErrBadStakebaseScriptLen
+	ErrBadStakebaseScriptLen = ErrorKind("ErrBadStakebaseScriptLen")
 
 	// ErrBadStakebaseScrVal indicates the signature script for a stakebase
 	// transaction was not set to the network consensus value.
-	ErrBadStakebaseScrVal
+	ErrBadStakebaseScrVal = ErrorKind("ErrBadStakebaseScrVal")
 
 	// ErrScriptMalformed indicates a transaction script is malformed in
 	// some way.  For example, it might be longer than the maximum allowed
 	// length or fail to parse.
-	ErrScriptMalformed
+	ErrScriptMalformed = ErrorKind("ErrScriptMalformed")
 
 	// ErrScriptValidation indicates the result of executing transaction
 	// script failed.  The error covers any failure when executing scripts
 	// such signature verification failures and execution past the end of
 	// the stack.
-	ErrScriptValidation
+	ErrScriptValidation = ErrorKind("ErrScriptValidation")
 
 	// ErrNotEnoughStake indicates that there was for some SStx in a given block,
 	// the given SStx did not have enough stake to meet the network target.
-	ErrNotEnoughStake
+	ErrNotEnoughStake = ErrorKind("ErrNotEnoughStake")
 
 	// ErrStakeBelowMinimum indicates that for some SStx in a given block,
 	// the given SStx had an amount of stake below the minimum network target.
-	ErrStakeBelowMinimum
+	ErrStakeBelowMinimum = ErrorKind("ErrStakeBelowMinimum")
 
 	// ErrNonstandardStakeTx indicates that a block contained a stake tx that
 	// was not one of the allowed types of a stake transactions.
-	ErrNonstandardStakeTx
+	ErrNonstandardStakeTx = ErrorKind("ErrNonstandardStakeTx")
 
 	// ErrNotEnoughVotes indicates that a block contained less than a majority
 	// of voters.
-	ErrNotEnoughVotes
+	ErrNotEnoughVotes = ErrorKind("ErrNotEnoughVotes")
 
 	// ErrTooManyVotes indicates that a block contained more than the maximum
 	// allowable number of votes.
-	ErrTooManyVotes
+	ErrTooManyVotes = ErrorKind("ErrTooManyVotes")
 
 	// ErrFreshStakeMismatch indicates that a block's header contained a different
 	// number of SStx as compared to what was found in the block.
-	ErrFreshStakeMismatch
+	ErrFreshStakeMismatch = ErrorKind("ErrFreshStakeMismatch")
 
 	// ErrTooManySStxs indicates that more than the allowed number of SStx was
 	// found in a block.
-	ErrTooManySStxs
+	ErrTooManySStxs = ErrorKind("ErrTooManySStxs")
 
 	// ErrInvalidEarlyStakeTx indicates that a tx type other than SStx was found
 	// in the stake tx tree before the period when stake validation begins, or
 	// before the stake tx type could possibly be included in the block.
-	ErrInvalidEarlyStakeTx
+	ErrInvalidEarlyStakeTx = ErrorKind("ErrInvalidEarlyStakeTx")
 
 	// ErrTicketUnavailable indicates that a vote in the block spent a ticket
 	// that could not be found.
-	ErrTicketUnavailable
+	ErrTicketUnavailable = ErrorKind("ErrTicketUnavailable")
 
 	// ErrVotesOnWrongBlock indicates that an SSGen voted on a block not the
 	// block's parent, and so was ineligible for inclusion into that block.
-	ErrVotesOnWrongBlock
+	ErrVotesOnWrongBlock = ErrorKind("ErrVotesOnWrongBlock")
 
 	// ErrVotesMismatch indicates that the number of SSGen in the block was not
 	// equivalent to the number of votes provided in the block header.
-	ErrVotesMismatch
+	ErrVotesMismatch = ErrorKind("ErrVotesMismatch")
 
 	// ErrIncongruentVotebit indicates that the first votebit in votebits was not
 	// the same as that determined by the majority of voters in the SSGen tx
 	// included in the block.
-	ErrIncongruentVotebit
+	ErrIncongruentVotebit = ErrorKind("ErrIncongruentVotebit")
 
 	// ErrInvalidSSRtx indicates than an SSRtx in a block could not be found to
 	// have a valid missed sstx input as per the stake ticket database.
-	ErrInvalidSSRtx
+	ErrInvalidSSRtx = ErrorKind("ErrInvalidSSRtx")
 
 	// ErrInvalidRevNum indicates that the number of revocations from the
 	// header was not the same as the number of SSRtx included in the block.
-	ErrRevocationsMismatch
+	ErrRevocationsMismatch = ErrorKind("ErrRevocationsMismatch")
 
 	// ErrTooManyRevocations indicates more revocations were found in a block
 	// than were allowed.
-	ErrTooManyRevocations
+	ErrTooManyRevocations = ErrorKind("ErrTooManyRevocations")
 
 	// ErrTicketCommitment indicates that a ticket commitment contains an amount
 	// that does not coincide with the associated ticket input amount.
-	ErrTicketCommitment
+	ErrTicketCommitment = ErrorKind("ErrTicketCommitment")
 
 	// ErrInvalidVoteInput indicates that an input to a vote transaction is
 	// either not a stake ticket submission or is not a supported version.
-	ErrInvalidVoteInput
+	ErrInvalidVoteInput = ErrorKind("ErrInvalidVoteInput")
 
 	// ErrBadNumPayees indicates that either a vote or revocation transaction
 	// does not make the correct number of payments per the associated ticket
 	// commitments.
-	ErrBadNumPayees
+	ErrBadNumPayees = ErrorKind("ErrBadNumPayees")
 
 	// ErrBadPayeeScriptVersion indicates that either a vote or revocation
 	// transaction output that corresponds to a ticket commitment does not use
 	// a supported script version.
-	ErrBadPayeeScriptVersion
+	ErrBadPayeeScriptVersion = ErrorKind("ErrBadPayeeScriptVersion")
 
 	// ErrBadPayeeScriptType indicates that either a vote or revocation
 	// transaction output that corresponds to a ticket commitment does not pay
 	// to the same script type required by the commitment.
-	ErrBadPayeeScriptType
+	ErrBadPayeeScriptType = ErrorKind("ErrBadPayeeScriptType")
 
 	// ErrBadPayeeScriptType indicates that either a vote or revocation
 	// transaction output that corresponds to a ticket commitment does not pay
 	// to the hash required by the commitment.
-	ErrMismatchedPayeeHash
+	ErrMismatchedPayeeHash = ErrorKind("ErrMismatchedPayeeHash")
 
 	// ErrBadPayeeValue indicates that either a vote or revocation transaction
 	// output that corresponds to a ticket commitment does not pay the expected
 	// amount required by the commitment.
-	ErrBadPayeeValue
+	ErrBadPayeeValue = ErrorKind("ErrBadPayeeValue")
 
 	// ErrSSGenSubsidy indicates that there was an error in the amount of subsidy
 	// generated in the vote.
-	ErrSSGenSubsidy
+	ErrSSGenSubsidy = ErrorKind("ErrSSGenSubsidy")
 
 	// ErrImmatureTicketSpend indicates that a vote or revocation is attempting
 	// to spend a ticket submission output that has not yet reached the required
 	// maturity.
-	ErrImmatureTicketSpend
+	ErrImmatureTicketSpend = ErrorKind("ErrImmatureTicketSpend")
 
 	// ErrTicketInputScript indicates that a ticket input is not one of the
 	// supported script forms or versions.
-	ErrTicketInputScript
+	ErrTicketInputScript = ErrorKind("ErrTicketInputScript")
 
 	// ErrInvalidRevokeInput indicates that an input to a revocation transaction
 	// is either not a stake ticket submission or is not a supported version.
-	ErrInvalidRevokeInput
+	ErrInvalidRevokeInput = ErrorKind("ErrInvalidRevokeInput")
 
 	// ErrSSRtxPayees indicates that the SSRtx failed to pay out to the committed
 	// addresses or amounts from the originating SStx.
-	ErrSSRtxPayees
+	ErrSSRtxPayees = ErrorKind("ErrSSRtxPayees")
 
 	// ErrTxSStxOutSpend indicates that a non SSGen or SSRtx tx attempted to spend
 	// an OP_SSTX tagged output from an SStx.
-	ErrTxSStxOutSpend
+	ErrTxSStxOutSpend = ErrorKind("ErrTxSStxOutSpend")
 
 	// ErrRegTxCreateStakeOut indicates that a regular tx attempted to create
 	// a stake tagged output.
-	ErrRegTxCreateStakeOut
+	ErrRegTxCreateStakeOut = ErrorKind("ErrRegTxCreateStakeOut")
 
 	// ErrInvalidFinalState indicates that the final state of the PRNG included
 	// in the block differed from the calculated final state.
-	ErrInvalidFinalState
+	ErrInvalidFinalState = ErrorKind("ErrInvalidFinalState")
 
 	// ErrPoolSize indicates an error in the ticket pool size for this block.
-	ErrPoolSize
+	ErrPoolSize = ErrorKind("ErrPoolSize")
 
 	// ErrForceReorgWrongChain indicates that a reroganization was attempted
 	// to be forced, but the chain indicated was not mirrored by b.bestChain.
-	ErrForceReorgWrongChain
+	ErrForceReorgWrongChain = ErrorKind("ErrForceReorgWrongChain")
 
 	// ErrForceReorgMissingChild indicates that a reroganization was attempted
 	// to be forced, but the child node to reorganize to could not be found.
-	ErrForceReorgMissingChild
+	ErrForceReorgMissingChild = ErrorKind("ErrForceReorgMissingChild")
 
 	// ErrBadStakebaseValue indicates that a block's stake tx tree has spent
 	// more than it is allowed.
-	ErrBadStakebaseValue
+	ErrBadStakebaseValue = ErrorKind("ErrBadStakebaseValue")
 
 	// ErrDiscordantTxTree specifies that a given origin tx's content
 	// indicated that it should exist in a different tx tree than the
 	// one given in the TxIn outpoint.
-	ErrDiscordantTxTree
+	ErrDiscordantTxTree = ErrorKind("ErrDiscordantTxTree")
 
 	// ErrStakeFees indicates an error with the fees found in the stake
 	// transaction tree.
-	ErrStakeFees
+	ErrStakeFees = ErrorKind("ErrStakeFees")
 
 	// ErrNoStakeTx indicates there were no stake transactions found in a
 	// block after stake validation height.
-	ErrNoStakeTx
+	ErrNoStakeTx = ErrorKind("ErrNoStakeTx")
 
 	// ErrBadBlockHeight indicates that a block header's embedded block height
 	// was different from where it was actually embedded in the block chain.
-	ErrBadBlockHeight
+	ErrBadBlockHeight = ErrorKind("ErrBadBlockHeight")
 
 	// ErrBlockOneTx indicates that block height 1 failed to correct generate
 	// the block one initial payout transaction.
-	ErrBlockOneTx
+	ErrBlockOneTx = ErrorKind("ErrBlockOneTx")
 
 	// ErrBlockOneTx indicates that block height 1 coinbase transaction in
 	// zero was incorrect in some way.
-	ErrBlockOneInputs
+	ErrBlockOneInputs = ErrorKind("ErrBlockOneInputs")
 
 	// ErrBlockOneOutputs indicates that block height 1 failed to incorporate
 	// the ledger addresses correctly into the transaction's outputs.
-	ErrBlockOneOutputs
+	ErrBlockOneOutputs = ErrorKind("ErrBlockOneOutputs")
 
 	// ErrNoTax indicates that there was no tax present in the coinbase of a
 	// block after height 1.
-	ErrNoTax
+	ErrNoTax = ErrorKind("ErrNoTax")
 
 	// ErrExpiredTx indicates that the transaction is currently expired.
-	ErrExpiredTx
+	ErrExpiredTx = ErrorKind("ErrExpiredTx")
 
 	// ErrExpiryTxSpentEarly indicates that an output from a transaction
 	// that included an expiry field was spent before coinbase maturity
 	// many blocks had passed in the blockchain.
-	ErrExpiryTxSpentEarly
+	ErrExpiryTxSpentEarly = ErrorKind("ErrExpiryTxSpentEarly")
 
 	// ErrFraudAmountIn indicates the witness amount given was fraudulent.
-	ErrFraudAmountIn
+	ErrFraudAmountIn = ErrorKind("ErrFraudAmountIn")
 
 	// ErrFraudBlockHeight indicates the witness block height given was
 	// fraudulent.
-	ErrFraudBlockHeight
+	ErrFraudBlockHeight = ErrorKind("ErrFraudBlockHeight")
 
 	// ErrFraudBlockIndex indicates the witness block index given was
 	// fraudulent.
-	ErrFraudBlockIndex
+	ErrFraudBlockIndex = ErrorKind("ErrFraudBlockIndex")
 
 	// ErrZeroValueOutputSpend indicates that a transaction attempted to spend a
 	// zero value output.
-	ErrZeroValueOutputSpend
+	ErrZeroValueOutputSpend = ErrorKind("ErrZeroValueOutputSpend")
 
 	// ErrInvalidEarlyVoteBits indicates that a block before stake validation
 	// height had an unallowed vote bits value.
-	ErrInvalidEarlyVoteBits
+	ErrInvalidEarlyVoteBits = ErrorKind("ErrInvalidEarlyVoteBits")
 
 	// ErrInvalidEarlyFinalState indicates that a block before stake validation
 	// height had a non-zero final state.
-	ErrInvalidEarlyFinalState
+	ErrInvalidEarlyFinalState = ErrorKind("ErrInvalidEarlyFinalState")
 
 	// ErrKnownInvalidBlock indicates that this block has previously failed
 	// validation.
-	ErrKnownInvalidBlock
+	ErrKnownInvalidBlock = ErrorKind("ErrKnownInvalidBlock")
 
 	// ErrInvalidAncestorBlock indicates that an ancestor of this block has
 	// failed validation.
-	ErrInvalidAncestorBlock
+	ErrInvalidAncestorBlock = ErrorKind("ErrInvalidAncestorBlock")
 
 	// ErrInvalidTemplateParent indicates that a block template builds on a
 	// block that is either not the current best chain tip or its parent.
-	ErrInvalidTemplateParent
+	ErrInvalidTemplateParent = ErrorKind("ErrInvalidTemplateParent")
 
 	// ErrUnknownPiKey indicates that the provided public Pi Key is not
 	// a well known key.
-	ErrUnknownPiKey
+	ErrUnknownPiKey = ErrorKind("ErrUnknownPiKey")
 
 	// ErrInvalidPiSignature indicates that a treasury spend transaction
 	// was not properly signed.
-	ErrInvalidPiSignature
+	ErrInvalidPiSignature = ErrorKind("ErrInvalidPiSignature")
 
 	// ErrInvalidTVoteWindow indicates that a treasury spend transaction
 	// appeared in a block that is prior to a valid treasury vote window.
-	ErrInvalidTVoteWindow
+	ErrInvalidTVoteWindow = ErrorKind("ErrInvalidTVoteWindow")
 
 	// ErrNotTVI indicates that a treasury spend transaction appeared in a
 	// block that is not at a TVI interval.
-	ErrNotTVI
+	ErrNotTVI = ErrorKind("ErrNotTVI")
 
 	// ErrInvalidTSpendWindow indicates that this treasury spend
 	// transaction is outside of the allowed window.
-	ErrInvalidTSpendWindow
+	ErrInvalidTSpendWindow = ErrorKind("ErrInvalidTSpendWindow")
 
 	// ErrNotEnoughTSpendVotes indicates that a treasury spend transaction
 	// does not have enough votes to be included in block.
-	ErrNotEnoughTSpendVotes
+	ErrNotEnoughTSpendVotes = ErrorKind("ErrNotEnoughTSpendVotes")
 
 	// ErrInvalidTSpendValueIn indicates that a treasury spend transaction
 	// ValueIn does not match the encoded copy in the first TxOut.
-	ErrInvalidTSpendValueIn
+	ErrInvalidTSpendValueIn = ErrorKind("ErrInvalidTSpendValueIn")
 
 	// ErrTSpendExists indicates that a duplicate treasury spend
 	// transaction has been mined on a TVI in the current best chain.
-	ErrTSpendExists
+	ErrTSpendExists = ErrorKind("ErrTSpendExists")
 
 	// ErrInvalidExpenditure indicates that a treasury spend transaction
 	// expenditure is out of range.
-	ErrInvalidExpenditure
+	ErrInvalidExpenditure = ErrorKind("ErrInvalidExpenditure")
 
 	// ErrFirstTxNotTreasurybase indicates the first transaction in a block
 	// is not a treasurybase transaction.
-	ErrFirstTxNotTreasurybase
+	ErrFirstTxNotTreasurybase = ErrorKind("ErrFirstTxNotTreasurybase")
 
 	// ErrBadTreasurybaseOutpoint indicates that the outpoint used by a
 	// treasurybase as input was non-null.
-	ErrBadTreasurybaseOutpoint
+	ErrBadTreasurybaseOutpoint = ErrorKind("ErrBadTreasurybaseOutpoint")
 
 	// ErrBadTreasurybaseFraudProof indicates that the fraud proof for a
 	// treasurybase input was non-null.
-	ErrBadTreasurybaseFraudProof
+	ErrBadTreasurybaseFraudProof = ErrorKind("ErrBadTreasurybaseFraudProof")
 
 	// ErrBadTreasurybaseScriptLen indicates the length of the signature script
 	// for a treasurybase transaction is not within the valid range.
-	ErrBadTreasurybaseScriptLen
+	ErrBadTreasurybaseScriptLen = ErrorKind("ErrBadTreasurybaseScriptLen")
 
 	// ErrTreasurybaseTxNotOpReturn indicates the second ouptut of a
 	// treasury base transaction is not an OP_RETURN.
-	ErrTreasurybaseTxNotOpReturn
+	ErrTreasurybaseTxNotOpReturn = ErrorKind("ErrTreasurybaseTxNotOpReturn")
 
 	// ErrTreasurybaseHeight indicates that the encoded height in the
 	// treasurybase is incorrect.
-	ErrTreasurybaseHeight
+	ErrTreasurybaseHeight = ErrorKind("ErrTreasurybaseHeight")
 
 	// ErrInvalidTreasurybaseTxOutputs indicates that the transaction does
 	// not have the correct number of outputs.
-	ErrInvalidTreasurybaseTxOutputs
+	ErrInvalidTreasurybaseTxOutputs = ErrorKind("ErrInvalidTreasurybaseTxOutputs")
 
 	// ErrInvalidTreasurybaseVersion indicates that the transaction output
 	// has the wrong version.
-	ErrInvalidTreasurybaseVersion
+	ErrInvalidTreasurybaseVersion = ErrorKind("ErrInvalidTreasurybaseVersion")
 
 	// ErrInvalidTreasurybaseScript indicates that the transaction output
 	// script is invalid.
-	ErrInvalidTreasurybaseScript
+	ErrInvalidTreasurybaseScript = ErrorKind("ErrInvalidTreasurybaseScript")
 
 	// ErrTreasurybaseOutValue ensures that the OP_TADD value of a
 	// treasurybase is not the expected amount.
-	ErrTreasurybaseOutValue
+	ErrTreasurybaseOutValue = ErrorKind("ErrTreasurybaseOutValue")
 
 	// ErrMultipleTreasurybases indicates a block contains more than one
 	// treasurybase transaction.
-	ErrMultipleTreasurybases
+	ErrMultipleTreasurybases = ErrorKind("ErrMultipleTreasurybases")
 
 	// ErrBadTreasurybaseAmountIn indicates that a block contains an
 	// invalid treasury contribution.
-	ErrBadTreasurybaseAmountIn
+	ErrBadTreasurybaseAmountIn = ErrorKind("ErrBadTreasurybaseAmountIn")
 
 	// ErrBadTSpendOutpoint indicates that the outpoint used by a
 	// treasury spend as input was non-null.
-	ErrBadTSpendOutpoint
+	ErrBadTSpendOutpoint = ErrorKind("ErrBadTSpendOutpoint")
 
 	// ErrBadTSpendFraudProof indicates that the fraud proof for a treasury
 	// spend transaction input was non-null.
-	ErrBadTSpendFraudProof
+	ErrBadTSpendFraudProof = ErrorKind("ErrBadTSpendFraudProof")
 
 	// ErrBadTSpendScriptLen indicates the length of the signature script
 	// for a treasury spend transaction is not within the valid range.
-	ErrBadTSpendScriptLen
+	ErrBadTSpendScriptLen = ErrorKind("ErrBadTSpendScriptLen")
 
 	// ErrInvalidTAddChange indicates the change output of a TAdd is zero.
-	ErrInvalidTAddChange
+	ErrInvalidTAddChange = ErrorKind("ErrInvalidTAddChange")
 
 	// ErrTooManyTAdds indicates the number of treasury adds in a given
 	// block is larger than the maximum allowed.
-	ErrTooManyTAdds
+	ErrTooManyTAdds = ErrorKind("ErrTooManyTAdds")
 
 	// ErrTicketExhaustion indicates extending a given block with another one
 	// would result in an unrecoverable chain due to ticket exhaustion.
-	ErrTicketExhaustion
-
-	// numErrorCodes is the maximum error code number used in tests.
-	numErrorCodes
+	ErrTicketExhaustion = ErrorKind("ErrTicketExhaustion")
 )
 
-// Map of ErrorCode values back to their constant names for pretty printing.
-var errorCodeStrings = map[ErrorCode]string{
-	ErrDuplicateBlock:               "ErrDuplicateBlock",
-	ErrMissingParent:                "ErrMissingParent",
-	ErrBlockTooBig:                  "ErrBlockTooBig",
-	ErrWrongBlockSize:               "ErrWrongBlockSize",
-	ErrBlockVersionTooOld:           "ErrBlockVersionTooOld",
-	ErrBadStakeVersion:              "ErrBadStakeVersion",
-	ErrInvalidTime:                  "ErrInvalidTime",
-	ErrTimeTooOld:                   "ErrTimeTooOld",
-	ErrTimeTooNew:                   "ErrTimeTooNew",
-	ErrDifficultyTooLow:             "ErrDifficultyTooLow",
-	ErrUnexpectedDifficulty:         "ErrUnexpectedDifficulty",
-	ErrHighHash:                     "ErrHighHash",
-	ErrBadMerkleRoot:                "ErrBadMerkleRoot",
-	ErrBadCommitmentRoot:            "ErrBadCommitmentRoot",
-	ErrBadCheckpoint:                "ErrBadCheckpoint",
-	ErrForkTooOld:                   "ErrForkTooOld",
-	ErrCheckpointTimeTooOld:         "ErrCheckpointTimeTooOld",
-	ErrNoTransactions:               "ErrNoTransactions",
-	ErrTooManyTransactions:          "ErrTooManyTransactions",
-	ErrNoTxInputs:                   "ErrNoTxInputs",
-	ErrNoTxOutputs:                  "ErrNoTxOutputs",
-	ErrInvalidTxOutputs:             "ErrInvalidTxOutputs",
-	ErrTxTooBig:                     "ErrTxTooBig",
-	ErrBadTxOutValue:                "ErrBadTxOutValue",
-	ErrDuplicateTxInputs:            "ErrDuplicateTxInputs",
-	ErrBadTxInput:                   "ErrBadTxInput",
-	ErrMissingTxOut:                 "ErrMissingTxOut",
-	ErrUnfinalizedTx:                "ErrUnfinalizedTx",
-	ErrDuplicateTx:                  "ErrDuplicateTx",
-	ErrOverwriteTx:                  "ErrOverwriteTx",
-	ErrImmatureSpend:                "ErrImmatureSpend",
-	ErrSpendTooHigh:                 "ErrSpendTooHigh",
-	ErrBadFees:                      "ErrBadFees",
-	ErrTooManySigOps:                "ErrTooManySigOps",
-	ErrFirstTxNotCoinbase:           "ErrFirstTxNotCoinbase",
-	ErrFirstTxNotOpReturn:           "ErrFirstTxNotOpReturn",
-	ErrCoinbaseHeight:               "ErrCoinbaseHeight",
-	ErrMultipleCoinbases:            "ErrMultipleCoinbases",
-	ErrStakeTxInRegularTree:         "ErrStakeTxInRegularTree",
-	ErrRegTxInStakeTree:             "ErrRegTxInStakeTree",
-	ErrBadCoinbaseScriptLen:         "ErrBadCoinbaseScriptLen",
-	ErrBadCoinbaseValue:             "ErrBadCoinbaseValue",
-	ErrBadCoinbaseOutpoint:          "ErrBadCoinbaseOutpoint",
-	ErrBadCoinbaseFraudProof:        "ErrBadCoinbaseFraudProof",
-	ErrBadCoinbaseAmountIn:          "ErrBadCoinbaseAmountIn",
-	ErrBadStakebaseAmountIn:         "ErrBadStakebaseAmountIn",
-	ErrBadStakebaseScriptLen:        "ErrBadStakebaseScriptLen",
-	ErrBadStakebaseScrVal:           "ErrBadStakebaseScrVal",
-	ErrScriptMalformed:              "ErrScriptMalformed",
-	ErrScriptValidation:             "ErrScriptValidation",
-	ErrNotEnoughStake:               "ErrNotEnoughStake",
-	ErrStakeBelowMinimum:            "ErrStakeBelowMinimum",
-	ErrNonstandardStakeTx:           "ErrNonstandardStakeTx",
-	ErrNotEnoughVotes:               "ErrNotEnoughVotes",
-	ErrTooManyVotes:                 "ErrTooManyVotes",
-	ErrFreshStakeMismatch:           "ErrFreshStakeMismatch",
-	ErrTooManySStxs:                 "ErrTooManySStxs",
-	ErrInvalidEarlyStakeTx:          "ErrInvalidEarlyStakeTx",
-	ErrTicketUnavailable:            "ErrTicketUnavailable",
-	ErrVotesOnWrongBlock:            "ErrVotesOnWrongBlock",
-	ErrVotesMismatch:                "ErrVotesMismatch",
-	ErrIncongruentVotebit:           "ErrIncongruentVotebit",
-	ErrInvalidSSRtx:                 "ErrInvalidSSRtx",
-	ErrRevocationsMismatch:          "ErrRevocationsMismatch",
-	ErrTooManyRevocations:           "ErrTooManyRevocations",
-	ErrTicketCommitment:             "ErrTicketCommitment",
-	ErrInvalidVoteInput:             "ErrInvalidVoteInput",
-	ErrBadNumPayees:                 "ErrBadNumPayees",
-	ErrBadPayeeScriptVersion:        "ErrBadPayeeScriptVersion",
-	ErrBadPayeeScriptType:           "ErrBadPayeeScriptType",
-	ErrMismatchedPayeeHash:          "ErrMismatchedPayeeHash",
-	ErrBadPayeeValue:                "ErrBadPayeeValue",
-	ErrSSGenSubsidy:                 "ErrSSGenSubsidy",
-	ErrImmatureTicketSpend:          "ErrImmatureTicketSpend",
-	ErrTicketInputScript:            "ErrTicketInputScript",
-	ErrInvalidRevokeInput:           "ErrInvalidRevokeInput",
-	ErrSSRtxPayees:                  "ErrSSRtxPayees",
-	ErrTxSStxOutSpend:               "ErrTxSStxOutSpend",
-	ErrRegTxCreateStakeOut:          "ErrRegTxCreateStakeOut",
-	ErrInvalidFinalState:            "ErrInvalidFinalState",
-	ErrPoolSize:                     "ErrPoolSize",
-	ErrForceReorgWrongChain:         "ErrForceReorgWrongChain",
-	ErrForceReorgMissingChild:       "ErrForceReorgMissingChild",
-	ErrBadStakebaseValue:            "ErrBadStakebaseValue",
-	ErrDiscordantTxTree:             "ErrDiscordantTxTree",
-	ErrStakeFees:                    "ErrStakeFees",
-	ErrNoStakeTx:                    "ErrNoStakeTx",
-	ErrBadBlockHeight:               "ErrBadBlockHeight",
-	ErrBlockOneTx:                   "ErrBlockOneTx",
-	ErrBlockOneInputs:               "ErrBlockOneInputs",
-	ErrBlockOneOutputs:              "ErrBlockOneOutputs",
-	ErrNoTax:                        "ErrNoTax",
-	ErrExpiredTx:                    "ErrExpiredTx",
-	ErrExpiryTxSpentEarly:           "ErrExpiryTxSpentEarly",
-	ErrFraudAmountIn:                "ErrFraudAmountIn",
-	ErrFraudBlockHeight:             "ErrFraudBlockHeight",
-	ErrFraudBlockIndex:              "ErrFraudBlockIndex",
-	ErrZeroValueOutputSpend:         "ErrZeroValueOutputSpend",
-	ErrInvalidEarlyVoteBits:         "ErrInvalidEarlyVoteBits",
-	ErrInvalidEarlyFinalState:       "ErrInvalidEarlyFinalState",
-	ErrKnownInvalidBlock:            "ErrKnownInvalidBlock",
-	ErrInvalidAncestorBlock:         "ErrInvalidAncestorBlock",
-	ErrUnknownPiKey:                 "ErrUnknownPiKey",
-	ErrInvalidPiSignature:           "ErrInvalidPiSignature",
-	ErrInvalidTVoteWindow:           "ErrInvalidTVoteWindow",
-	ErrNotTVI:                       "ErrNotTVI",
-	ErrInvalidTSpendWindow:          "ErrInvalidTSpendWindow",
-	ErrNotEnoughTSpendVotes:         "ErrNotEnoughTSpendVotes",
-	ErrInvalidTSpendValueIn:         "ErrInvalidTSpendValueIn",
-	ErrTSpendExists:                 "ErrTSpendExists",
-	ErrInvalidExpenditure:           "ErrInvalidExpenditure",
-	ErrFirstTxNotTreasurybase:       "ErrFirstTxNotTreasurybase",
-	ErrBadTreasurybaseOutpoint:      "ErrBadTreasurybaseOutpoint",
-	ErrBadTreasurybaseFraudProof:    "ErrBadTreasurybaseFraudProof",
-	ErrBadTreasurybaseScriptLen:     "ErrBadTreasurybaseScriptLen",
-	ErrTreasurybaseTxNotOpReturn:    "ErrTreasurybaseTxNotOpReturn",
-	ErrTreasurybaseHeight:           "ErrTreasurybaseHeight",
-	ErrTreasurybaseOutValue:         "ErrTreasurybaseOutValue",
-	ErrInvalidTreasurybaseTxOutputs: "ErrInvalidTreasurybaseTxOutputs",
-	ErrInvalidTreasurybaseVersion:   "ErrInvalidTreasurybaseVersion",
-	ErrInvalidTreasurybaseScript:    "ErrInvalidTreasurybaseScript",
-	ErrMultipleTreasurybases:        "ErrMultipleTreasurybases",
-	ErrBadTreasurybaseAmountIn:      "ErrBadTreasurybaseAmountIn",
-	ErrBadTSpendOutpoint:            "ErrBadTSpendOutpoint",
-	ErrBadTSpendFraudProof:          "ErrBadTSpendFraudProof",
-	ErrBadTSpendScriptLen:           "ErrBadTSpendScriptLen",
-	ErrInvalidTemplateParent:        "ErrInvalidTemplateParent",
-	ErrInvalidTAddChange:            "ErrInvalidTAddChange",
-	ErrTooManyTAdds:                 "ErrTooManyTAdds",
-	ErrTicketExhaustion:             "ErrTicketExhaustion",
-}
-
-// String returns the ErrorCode as a human-readable name.
-func (e ErrorCode) String() string {
-	if s := errorCodeStrings[e]; s != "" {
-		return s
-	}
-	return fmt.Sprintf("Unknown ErrorCode (%d)", int(e))
+// Error satisfies the error interface and prints human-readable errors.
+func (e ErrorKind) Error() string {
+	return string(e)
 }
 
-// RuleError identifies a rule violation.  It is used to indicate that
-// processing of a block or transaction failed due to one of the many validation
-// rules.  The caller can use type assertions to determine if a failure was
-// specifically due to a rule violation and access the ErrorCode field to
-// ascertain the specific reason for the rule violation.
+// RuleError identifies a rule violation related to blocks. It has
+// full support for errors.Is and errors.As, so the caller can ascertain the
+// specific reason for the error by checking the underlying error.
 type RuleError struct {
-	ErrorCode   ErrorCode // Describes the kind of error
-	Description string    // Human readable description of the issue
+	Err         error
+	Description string
 }
 
 // Error satisfies the error interface and prints human-readable errors.
@@ -797,14 +656,12 @@ func (e RuleError) Error() string {
 	return e.Description
 }
 
-// ruleError creates a RuleError given a set of arguments.
-func ruleError(c ErrorCode, desc string) RuleError {
-	return RuleError{ErrorCode: c, Description: desc}
+// Unwrap returns the underlying wrapped error.
+func (e RuleError) Unwrap() error {
+	return e.Err
 }
 
-// IsErrorCode returns whether or not the provided error is a rule error with
-// the provided error code.
-func IsErrorCode(err error, c ErrorCode) bool {
-	var e RuleError
-	return errors.As(err, &e) && e.ErrorCode == c
+// ruleError creates a RuleError given a set of arguments.
+func ruleError(kind ErrorKind, desc string) RuleError {
+	return RuleError{Err: kind, Description: desc}
 }
diff --git a/blockchain/go.mod b/blockchain/go.mod
index 46724656..45b4fcb1 100644
--- a/blockchain/go.mod
+++ b/blockchain/go.mod
@@ -3,16 +3,16 @@ module github.com/decred/dcrd/blockchain/v3
 go 1.13
 
 require (
-	github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200921173733-67245079e9fb
+	github.com/decred/dcrd/blockchain/stake/v3 v3.0.0
 	github.com/decred/dcrd/blockchain/standalone/v2 v2.0.0-20200921173733-67245079e9fb
 	github.com/decred/dcrd/chaincfg/chainhash v1.0.2
 	github.com/decred/dcrd/chaincfg/v3 v3.0.0
-	github.com/decred/dcrd/database/v2 v2.0.1
+	github.com/decred/dcrd/database/v2 v2.0.2
 	github.com/decred/dcrd/dcrec v1.0.0
 	github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0
 	github.com/decred/dcrd/dcrutil/v3 v3.0.0
 	github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200921173733-67245079e9fb
-	github.com/decred/dcrd/txscript/v3 v3.0.0-20200921173733-67245079e9fb
+	github.com/decred/dcrd/txscript/v3 v3.0.0
 	github.com/decred/dcrd/wire v1.4.0
 	github.com/decred/slog v1.1.0
 )
diff --git a/blockchain/go.sum b/blockchain/go.sum
index 809ed72e..d6bddbe3 100644
--- a/blockchain/go.sum
+++ b/blockchain/go.sum
@@ -1,68 +1,100 @@
 github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
 github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
-github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4=
-github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
-github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE=
-github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
 github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
-github.com/decred/base58 v1.0.1 h1:w5qTcb0hYpKuIBYIn4Ckirkj1aOWrSq8onPQpb3eGg8=
-github.com/decred/base58 v1.0.1/go.mod h1:H2ENcsJjye1G7CbRa67kV9OFaui0LGr56ntKKoY5g9c=
 github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI=
 github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E=
 github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU=
 github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
-github.com/decred/dcrd/chaincfg/v2 v2.3.0 h1:ItmU+7DeUtyiabrcW+16MJFgY/BBeeYaPfkBLrFLyjo=
-github.com/decred/dcrd/chaincfg/v2 v2.3.0/go.mod h1:7qUJTvn+y/kswSRZ4sT2+EmvlDTDyy2InvNFtX/hxk0=
 github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/crypto/ripemd160 v1.0.0 h1:MciTnR4NfBqDFRFjFkrn8WPLP4Vo7t6ww6ghfn6wcXQ=
-github.com/decred/dcrd/crypto/ripemd160 v1.0.0/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg=
 github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc=
 github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg=
-github.com/decred/dcrd/database/v2 v2.0.1 h1:ghLzkKpVpwvjrdRv3njrEfkvygQpYQX66sGVs8ha+E8=
-github.com/decred/dcrd/database/v2 v2.0.1/go.mod h1:ZOaWTv3IlNqCA+y7q3q5EozgmiDOmNwCSq3ntZn2CDo=
+github.com/decred/dcrd/database/v2 v2.0.2 h1:t1ch4sk2qIhxGcAmWQJkFwsbqKITEcVa8E+BFpxOf7s=
+github.com/decred/dcrd/database/v2 v2.0.2/go.mod h1:S78KbTCCJWUTJDVTByiQuB+HmL0DM2vIMsa2WsrF9KM=
 github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o=
 github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8=
-github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0 h1:E5KszxGgpjpmW8vN811G6rBAZg0/S/DftdGqN4FW5x4=
-github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc=
 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1 h1:V6eqU1crZzuoFT4KG2LhaU5xDSdkHuvLQsj25wd7Wb4=
 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc=
-github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY=
-github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY=
-github.com/decred/dcrd/dcrutil/v2 v2.0.1 h1:aL+c7o7Q66HV1gIif+XkNYo9DeorN3l01Vns8mh0mqs=
-github.com/decred/dcrd/dcrutil/v2 v2.0.1/go.mod h1:JdEgF6eh0TTohPeiqDxqDSikTSvAczq0J7tFMyyeD+k=
-github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM=
 github.com/decred/dcrd/wire v1.4.0 h1:KmSo6eTQIvhXS0fLBQ/l7hG7QLcSJQKSwSyzSqJYDk0=
 github.com/decred/dcrd/wire v1.4.0/go.mod h1:WxC/0K+cCAnBh+SKsRjIX9YPgvrjhmE+6pZlel1G7Ro=
-github.com/decred/slog v1.0.0 h1:Dl+W8O6/JH6n2xIFN2p3DNjCmjYwvrXsjlSJTQQ4MhE=
-github.com/decred/slog v1.0.0/go.mod h1:zR98rEZHSnbZ4WHZtO0iqmSZjDLKhkXfrPTZQKtAonQ=
 github.com/decred/slog v1.1.0 h1:uz5ZFfmaexj1rEDgZvzQ7wjGkoSPjw2LCh8K+K1VrW4=
 github.com/decred/slog v1.1.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
-github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-golang.org/x/net v0.0.0-20180719180050-a680a1efc54d h1:i6RB+Qz1ug7TvJdY4zieRMpnLAtkHSHTOvApNXfLT4A=
-golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca h1:Ld/zXl5t4+D69SiV4JoN7kkfvJdOWlPpfxrzxpLMoUk=
+github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/blockchain/stake/error.go b/blockchain/stake/error.go
index 88bef99e..aa08efb7 100644
--- a/blockchain/stake/error.go
+++ b/blockchain/stake/error.go
@@ -1,5 +1,5 @@
 // Copyright (c) 2014 Conformal Systems LLC.
-// Copyright (c) 2015-2016 The Decred developers
+// Copyright (c) 2015-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/blockchain/stake/go.mod b/blockchain/stake/go.mod
index 2fbc5382..ec0d665c 100644
--- a/blockchain/stake/go.mod
+++ b/blockchain/stake/go.mod
@@ -5,18 +5,11 @@ go 1.13
 require (
 	github.com/decred/dcrd/chaincfg/chainhash v1.0.2
 	github.com/decred/dcrd/chaincfg/v3 v3.0.0
-	github.com/decred/dcrd/database/v2 v2.0.1
+	github.com/decred/dcrd/database/v2 v2.0.2
 	github.com/decred/dcrd/dcrec v1.0.0
 	github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0
 	github.com/decred/dcrd/dcrutil/v3 v3.0.0
-	github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986
+	github.com/decred/dcrd/txscript/v3 v3.0.0
 	github.com/decred/dcrd/wire v1.4.0
 	github.com/decred/slog v1.1.0
 )
-
-replace (
-	github.com/decred/dcrd/chaincfg/v3 => ../../chaincfg
-	github.com/decred/dcrd/dcrec/secp256k1/v3 => ../../dcrec/secp256k1
-	github.com/decred/dcrd/dcrutil/v3 => ../../dcrutil
-	github.com/decred/dcrd/txscript/v3 => ../../txscript
-)
diff --git a/blockchain/stake/go.sum b/blockchain/stake/go.sum
index 809ed72e..c9eb645e 100644
--- a/blockchain/stake/go.sum
+++ b/blockchain/stake/go.sum
@@ -1,68 +1,110 @@
 github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
 github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
-github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4=
-github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
-github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE=
-github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
 github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
-github.com/decred/base58 v1.0.1 h1:w5qTcb0hYpKuIBYIn4Ckirkj1aOWrSq8onPQpb3eGg8=
-github.com/decred/base58 v1.0.1/go.mod h1:H2ENcsJjye1G7CbRa67kV9OFaui0LGr56ntKKoY5g9c=
 github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI=
 github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E=
 github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU=
 github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
-github.com/decred/dcrd/chaincfg/v2 v2.3.0 h1:ItmU+7DeUtyiabrcW+16MJFgY/BBeeYaPfkBLrFLyjo=
-github.com/decred/dcrd/chaincfg/v2 v2.3.0/go.mod h1:7qUJTvn+y/kswSRZ4sT2+EmvlDTDyy2InvNFtX/hxk0=
+github.com/decred/dcrd/chaincfg/v3 v3.0.0 h1:+TFbu7ZmvBwM+SZz5mrj6cun9ts/6DAL5sqnsaFBHGQ=
+github.com/decred/dcrd/chaincfg/v3 v3.0.0/go.mod h1:EspyubQ7D2w6tjP7rBGDIE7OTbuMgBjR2F2kZFnh31A=
 github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/crypto/ripemd160 v1.0.0 h1:MciTnR4NfBqDFRFjFkrn8WPLP4Vo7t6ww6ghfn6wcXQ=
-github.com/decred/dcrd/crypto/ripemd160 v1.0.0/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg=
 github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc=
 github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg=
-github.com/decred/dcrd/database/v2 v2.0.1 h1:ghLzkKpVpwvjrdRv3njrEfkvygQpYQX66sGVs8ha+E8=
-github.com/decred/dcrd/database/v2 v2.0.1/go.mod h1:ZOaWTv3IlNqCA+y7q3q5EozgmiDOmNwCSq3ntZn2CDo=
+github.com/decred/dcrd/database/v2 v2.0.2 h1:t1ch4sk2qIhxGcAmWQJkFwsbqKITEcVa8E+BFpxOf7s=
+github.com/decred/dcrd/database/v2 v2.0.2/go.mod h1:S78KbTCCJWUTJDVTByiQuB+HmL0DM2vIMsa2WsrF9KM=
 github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o=
 github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8=
-github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0 h1:E5KszxGgpjpmW8vN811G6rBAZg0/S/DftdGqN4FW5x4=
-github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc=
 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1 h1:V6eqU1crZzuoFT4KG2LhaU5xDSdkHuvLQsj25wd7Wb4=
 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc=
-github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY=
-github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY=
-github.com/decred/dcrd/dcrutil/v2 v2.0.1 h1:aL+c7o7Q66HV1gIif+XkNYo9DeorN3l01Vns8mh0mqs=
-github.com/decred/dcrd/dcrutil/v2 v2.0.1/go.mod h1:JdEgF6eh0TTohPeiqDxqDSikTSvAczq0J7tFMyyeD+k=
-github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM=
+github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw=
+github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
+github.com/decred/dcrd/dcrutil/v3 v3.0.0 h1:n6uQaTQynIhCY89XsoDk2WQqcUcnbD+zUM9rnZcIOZo=
+github.com/decred/dcrd/dcrutil/v3 v3.0.0/go.mod h1:iVsjcqVzLmYFGCZLet2H7Nq+7imV9tYcuY+0lC2mNsY=
+github.com/decred/dcrd/txscript/v3 v3.0.0 h1:74NmirXAIskbGP0g9OWtrmN7OxDbWJ9G73a5uoxTkcM=
+github.com/decred/dcrd/txscript/v3 v3.0.0/go.mod h1:pdvnlD4KGdDoc09cvWRJ8EoRQUaiUz41uDevOWuEfII=
 github.com/decred/dcrd/wire v1.4.0 h1:KmSo6eTQIvhXS0fLBQ/l7hG7QLcSJQKSwSyzSqJYDk0=
 github.com/decred/dcrd/wire v1.4.0/go.mod h1:WxC/0K+cCAnBh+SKsRjIX9YPgvrjhmE+6pZlel1G7Ro=
-github.com/decred/slog v1.0.0 h1:Dl+W8O6/JH6n2xIFN2p3DNjCmjYwvrXsjlSJTQQ4MhE=
-github.com/decred/slog v1.0.0/go.mod h1:zR98rEZHSnbZ4WHZtO0iqmSZjDLKhkXfrPTZQKtAonQ=
 github.com/decred/slog v1.1.0 h1:uz5ZFfmaexj1rEDgZvzQ7wjGkoSPjw2LCh8K+K1VrW4=
 github.com/decred/slog v1.1.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
-github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-golang.org/x/net v0.0.0-20180719180050-a680a1efc54d h1:i6RB+Qz1ug7TvJdY4zieRMpnLAtkHSHTOvApNXfLT4A=
-golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca h1:Ld/zXl5t4+D69SiV4JoN7kkfvJdOWlPpfxrzxpLMoUk=
+github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/blockchain/stake/internal/ticketdb/chainio.go b/blockchain/stake/internal/ticketdb/chainio.go
index f8374f0a..1e60e76c 100644
--- a/blockchain/stake/internal/ticketdb/chainio.go
+++ b/blockchain/stake/internal/ticketdb/chainio.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2019 The Decred developers
+// Copyright (c) 2015-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/blockchain/stake/lottery.go b/blockchain/stake/lottery.go
index af7b54d9..708c42c5 100644
--- a/blockchain/stake/lottery.go
+++ b/blockchain/stake/lottery.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2017 The Decred developers
+// Copyright (c) 2015-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/blockchain/stake/staketx.go b/blockchain/stake/staketx.go
index 81c62d4c..9cf818e6 100644
--- a/blockchain/stake/staketx.go
+++ b/blockchain/stake/staketx.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2019 The Decred developers
+// Copyright (c) 2015-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 //
diff --git a/blockchain/stake/tickets.go b/blockchain/stake/tickets.go
index adab2a77..7e8011e5 100644
--- a/blockchain/stake/tickets.go
+++ b/blockchain/stake/tickets.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2019 The Decred developers
+// Copyright (c) 2015-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/blockmanager.go b/blockmanager.go
index 017ac44b..a9a226c4 100644
--- a/blockmanager.go
+++ b/blockmanager.go
@@ -662,76 +662,63 @@ func (b *blockManager) handleDonePeerMsg(peer *peerpkg.Peer) {
 // given error. This function can convert some select blockchain and mempool
 // error types to the historical rejection codes used on the p2p wire protocol.
 func errToWireRejectCode(err error) (wire.RejectCode, string) {
-	// Unwrap mempool errors.
-	var rerr mempool.RuleError
-	if errors.As(err, &rerr) {
-		err = rerr.Err
-	}
-
 	// The default reason to reject a transaction/block is due to it being
 	// invalid somehow.
 	code := wire.RejectInvalid
 	var reason string
 
-	var berr blockchain.RuleError
-	var terr mempool.TxRuleError
+	// Convert recognized errors to a reject code.
 	switch {
-	case errors.As(err, &berr):
-		// Convert the chain error to a reject code.
-		switch berr.ErrorCode {
-		// Rejected due to duplicate.
-		case blockchain.ErrDuplicateBlock:
-			code = wire.RejectDuplicate
-
-		// Rejected due to obsolete version.
-		case blockchain.ErrBlockVersionTooOld:
-			code = wire.RejectObsolete
-
-		// Rejected due to checkpoint.
-		case blockchain.ErrCheckpointTimeTooOld,
-			blockchain.ErrDifficultyTooLow,
-			blockchain.ErrBadCheckpoint,
-			blockchain.ErrForkTooOld:
-
-			code = wire.RejectCheckpoint
-		}
-
-		reason = berr.Error()
-	case errors.As(err, &terr):
-		switch terr.ErrorCode {
-		// Error codes which map to a duplicate transaction already
-		// mined or in the mempool.
-		case mempool.ErrMempoolDoubleSpend,
-			mempool.ErrAlreadyVoted,
-			mempool.ErrDuplicate,
-			mempool.ErrTooManyVotes,
-			mempool.ErrDuplicateRevocation,
-			mempool.ErrAlreadyExists,
-			mempool.ErrOrphan:
-
-			code = wire.RejectDuplicate
-
-		// Error codes which map to a non-standard transaction being
-		// relayed.
-		case mempool.ErrOrphanPolicyViolation,
-			mempool.ErrOldVote,
-			mempool.ErrSeqLockUnmet,
-			mempool.ErrNonStandard:
-
-			code = wire.RejectNonstandard
-
-		// Error codes which map to an insufficient fee being paid.
-		case mempool.ErrInsufficientFee,
-			mempool.ErrInsufficientPriority:
-
-			code = wire.RejectInsufficientFee
-
-		// Error codes which map to an attempt to create dust outputs.
-		case mempool.ErrDustOutput:
-			code = wire.RejectDust
-		}
-
-		reason = terr.Error()
+	// Rejected due to duplicate.
+	case errors.Is(err, blockchain.ErrDuplicateBlock):
+		code = wire.RejectDuplicate
+		reason = err.Error()
+
+	// Rejected due to obsolete version.
+	case errors.Is(err, blockchain.ErrBlockVersionTooOld):
+		code = wire.RejectObsolete
+		reason = err.Error()
+
+	// Rejected due to checkpoint.
+	case errors.Is(err, blockchain.ErrCheckpointTimeTooOld),
+		errors.Is(err, blockchain.ErrDifficultyTooLow),
+		errors.Is(err, blockchain.ErrBadCheckpoint),
+		errors.Is(err, blockchain.ErrForkTooOld):
+		code = wire.RejectCheckpoint
+		reason = err.Error()
+
+	// Error codes which map to a duplicate transaction already
+	// mined or in the mempool.
+	case errors.Is(err, mempool.ErrMempoolDoubleSpend),
+		errors.Is(err, mempool.ErrAlreadyVoted),
+		errors.Is(err, mempool.ErrDuplicate),
+		errors.Is(err, mempool.ErrTooManyVotes),
+		errors.Is(err, mempool.ErrDuplicateRevocation),
+		errors.Is(err, mempool.ErrAlreadyExists),
+		errors.Is(err, mempool.ErrOrphan):
+		code = wire.RejectDuplicate
+		reason = err.Error()
+
+	// Error codes which map to a non-standard transaction being
+	// relayed.
+	case errors.Is(err, mempool.ErrOrphanPolicyViolation),
+		errors.Is(err, mempool.ErrOldVote),
+		errors.Is(err, mempool.ErrSeqLockUnmet),
+		errors.Is(err, mempool.ErrNonStandard):
+		code = wire.RejectNonstandard
+		reason = err.Error()
+
+	// Error codes which map to an insufficient fee being paid.
+	case errors.Is(err, mempool.ErrInsufficientFee),
+		errors.Is(err, mempool.ErrInsufficientPriority):
+		code = wire.RejectInsufficientFee
+		reason = err.Error()
+
+	// Error codes which map to an attempt to create dust outputs.
+	case errors.Is(err, mempool.ErrDustOutput):
+		code = wire.RejectDust
+		reason = err.Error()
+
 	default:
 		reason = fmt.Sprintf("rejected: %v", err)
 	}
@@ -1005,7 +992,7 @@ func (b *blockManager) processBlockAndOrphans(block *dcrutil.Block, flags blockc
 	// returned indicates the block is an orphan.
 	blockHash := block.Hash()
 	forkLen, err := b.cfg.Chain.ProcessBlock(block, flags)
-	if blockchain.IsErrorCode(err, blockchain.ErrMissingParent) {
+	if errors.Is(err, blockchain.ErrMissingParent) {
 		bmgrLog.Infof("Adding orphan block %v with parent %v", blockHash,
 			block.MsgBlock().Header.PrevBlock)
 		b.addOrphanBlock(block)
@@ -1846,26 +1833,16 @@ func headerApprovesParent(header *wire.BlockHeader) bool {
 // is expected to have come from mempool, indicates a transaction was rejected
 // either due to containing a double spend or already existing in the pool.
 func isDoubleSpendOrDuplicateError(err error) bool {
-	var merr mempool.RuleError
-	if !errors.As(err, &merr) {
-		return false
-	}
-
-	var rerr mempool.TxRuleError
-	if errors.As(merr.Err, &rerr) {
-		switch rerr.ErrorCode {
-		case mempool.ErrDuplicate:
-			return true
-		case mempool.ErrAlreadyExists:
-			return true
-		default:
-			return false
-		}
+	switch {
+	case errors.Is(err, mempool.ErrDuplicate):
+		return true
+	case errors.Is(err, mempool.ErrAlreadyExists):
+		return true
+	case errors.Is(err, blockchain.ErrMissingTxOut):
+		return true
 	}
 
-	var cerr blockchain.RuleError
-	return errors.As(merr.Err, &cerr) &&
-		cerr.ErrorCode == blockchain.ErrMissingTxOut
+	return false
 }
 
 // handleBlockchainNotification handles notifications from blockchain.  It does
@@ -2051,6 +2028,9 @@ func (b *blockManager) handleBlockchainNotification(notification *blockchain.Not
 				b.cfg.PeerNotifier.TransactionConfirmed(tx)
 			}
 		}
+
+		// Add regular transactions back to the mempool,
+		// excluding the coinbase since it does not belong in the mempool.
 		handleConnectedBlockTxns(block.Transactions()[1:])
 		if isTreasuryEnabled {
 			// Skip treasurybase
diff --git a/cmd/addblock/import.go b/cmd/addblock/import.go
index fa5c4cb5..74ac43ab 100644
--- a/cmd/addblock/import.go
+++ b/cmd/addblock/import.go
@@ -8,6 +8,7 @@ package main
 import (
 	"context"
 	"encoding/binary"
+	"errors"
 	"fmt"
 	"io"
 	"sync"
@@ -129,7 +130,7 @@ func (bi *blockImporter) processBlock(serializedBlock []byte) (bool, error) {
 	// known checkpoints.
 	forkLen, err := bi.chain.ProcessBlock(block, blockchain.BFFastAdd)
 	if err != nil {
-		if blockchain.IsErrorCode(err, blockchain.ErrMissingParent) {
+		if errors.Is(err, blockchain.ErrMissingParent) {
 			return false, fmt.Errorf("import file contains an orphan block: %v",
 				blockHash)
 		}
diff --git a/docs/README.md b/docs/README.md
index 662ebf9c..aca6af1e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -184,7 +184,7 @@ The following versioned modules are provided by dcrd repository:
   base for creating and managing Decred network peers
 * [blockchain/v2](https://github.com/decred/dcrd/tree/master/blockchain) -
   Implements Decred block handling and chain selection rules
-  * [stake/v2](https://github.com/decred/dcrd/tree/master/blockchain/stake) -
+  * [stake/v3](https://github.com/decred/dcrd/tree/master/blockchain/stake) -
     Provides an API for working with stake transactions and other portions
     related to the Proof-of-Stake (PoS) system
   * [standalone/v2](https://github.com/decred/dcrd/tree/master/blockchain/standalone) -
diff --git a/docs/assets/module_hierarchy.gv b/docs/assets/module_hierarchy.gv
index 9ae542a3..4b8553d6 100644
--- a/docs/assets/module_hierarchy.gv
+++ b/docs/assets/module_hierarchy.gv
@@ -17,7 +17,7 @@ digraph {
 	database [label="database/v2" fillcolor=dodgerblue]
 	hdkeychain [label="hdkeychain/v3" fillcolor=deepskyblue]
 	txscript [label="txscript/v3" fillcolor=mediumspringgreen]
-	stake [label="blockchain/stake/v2" fillcolor=bisque]
+	stake [label="blockchain/stake/v3" fillcolor=bisque]
 	standalone [label="blockchain/standalone/v2" fillcolor=firebrick4]
 	gcs [label="gcs" fillcolor=gold]
 	blockchain [label="blockchain/v2" fillcolor=orchid]
diff --git a/docs/assets/module_hierarchy.svg b/docs/assets/module_hierarchy.svg
index 1169d97e..643d3221 100644
--- a/docs/assets/module_hierarchy.svg
+++ b/docs/assets/module_hierarchy.svg
@@ -198,7 +198,7 @@
 <!-- stake -->
 <g id="node16" class="node"><title>stake</title>
 <path fill="bisque" stroke="black" d="M294,-324C294,-324 191,-324 191,-324 185,-324 179,-318 179,-312 179,-312 179,-300 179,-300 179,-294 185,-288 191,-288 191,-288 294,-288 294,-288 300,-288 306,-294 306,-300 306,-300 306,-312 306,-312 306,-318 300,-324 294,-324"/>
-<text text-anchor="middle" x="242.5" y="-302.3" font-family="Times New Roman,serif" font-size="14.00">blockchain/stake/v2</text>
+<text text-anchor="middle" x="242.5" y="-302.3" font-family="Times New Roman,serif" font-size="14.00">blockchain/stake/v3</text>
 </g>
 <!-- database&#45;&gt;stake -->
 <g id="edge15" class="edge"><title>database&#45;&gt;stake</title>
diff --git a/gcs/blockcf/blockcf.go b/gcs/blockcf/blockcf.go
index 7b97d344..aca1101d 100644
--- a/gcs/blockcf/blockcf.go
+++ b/gcs/blockcf/blockcf.go
@@ -1,6 +1,6 @@
 // Copyright (c) 2017 The btcsuite developers
 // Copyright (c) 2017 The Lightning Network Developers
-// Copyright (c) 2018-2019 The Decred developers
+// Copyright (c) 2018-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/gcs/blockcf2/blockcf.go b/gcs/blockcf2/blockcf.go
index 0cf5e583..07ba55d0 100644
--- a/gcs/blockcf2/blockcf.go
+++ b/gcs/blockcf2/blockcf.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 The Decred developers
+// Copyright (c) 2019-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/gcs/go.mod b/gcs/go.mod
index f186ddb2..cbabb316 100644
--- a/gcs/go.mod
+++ b/gcs/go.mod
@@ -4,17 +4,9 @@ go 1.11
 
 require (
 	github.com/dchest/siphash v1.2.1
-	github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200215031403-6b2ce76f0986
+	github.com/decred/dcrd/blockchain/stake/v3 v3.0.0
 	github.com/decred/dcrd/chaincfg/chainhash v1.0.2
 	github.com/decred/dcrd/crypto/blake256 v1.0.0
-	github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986
+	github.com/decred/dcrd/txscript/v3 v3.0.0
 	github.com/decred/dcrd/wire v1.4.0
 )
-
-replace (
-	github.com/decred/dcrd/blockchain/stake/v3 => ../blockchain/stake
-	github.com/decred/dcrd/chaincfg/v3 => ../chaincfg
-	github.com/decred/dcrd/dcrec/secp256k1/v3 => ../dcrec/secp256k1
-	github.com/decred/dcrd/dcrutil/v3 => ../dcrutil
-	github.com/decred/dcrd/txscript/v3 => ../txscript
-)
diff --git a/gcs/go.sum b/gcs/go.sum
index 809ed72e..859fdfee 100644
--- a/gcs/go.sum
+++ b/gcs/go.sum
@@ -1,68 +1,110 @@
 github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
 github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
-github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4=
-github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
-github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE=
-github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
 github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
-github.com/decred/base58 v1.0.1 h1:w5qTcb0hYpKuIBYIn4Ckirkj1aOWrSq8onPQpb3eGg8=
-github.com/decred/base58 v1.0.1/go.mod h1:H2ENcsJjye1G7CbRa67kV9OFaui0LGr56ntKKoY5g9c=
 github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI=
 github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E=
+github.com/decred/dcrd/blockchain/stake/v3 v3.0.0 h1:vr0o0ICjuEzg1End6YtBfwgDuPkg+FYIwGVEz18kFg0=
+github.com/decred/dcrd/blockchain/stake/v3 v3.0.0/go.mod h1:5GIUwsrHQCJauacgCegIR6t92SaeVi28Qls/BLN9vOw=
 github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU=
 github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
-github.com/decred/dcrd/chaincfg/v2 v2.3.0 h1:ItmU+7DeUtyiabrcW+16MJFgY/BBeeYaPfkBLrFLyjo=
-github.com/decred/dcrd/chaincfg/v2 v2.3.0/go.mod h1:7qUJTvn+y/kswSRZ4sT2+EmvlDTDyy2InvNFtX/hxk0=
+github.com/decred/dcrd/chaincfg/v3 v3.0.0 h1:+TFbu7ZmvBwM+SZz5mrj6cun9ts/6DAL5sqnsaFBHGQ=
+github.com/decred/dcrd/chaincfg/v3 v3.0.0/go.mod h1:EspyubQ7D2w6tjP7rBGDIE7OTbuMgBjR2F2kZFnh31A=
 github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/crypto/ripemd160 v1.0.0 h1:MciTnR4NfBqDFRFjFkrn8WPLP4Vo7t6ww6ghfn6wcXQ=
-github.com/decred/dcrd/crypto/ripemd160 v1.0.0/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg=
 github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc=
 github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg=
-github.com/decred/dcrd/database/v2 v2.0.1 h1:ghLzkKpVpwvjrdRv3njrEfkvygQpYQX66sGVs8ha+E8=
-github.com/decred/dcrd/database/v2 v2.0.1/go.mod h1:ZOaWTv3IlNqCA+y7q3q5EozgmiDOmNwCSq3ntZn2CDo=
+github.com/decred/dcrd/database/v2 v2.0.2 h1:t1ch4sk2qIhxGcAmWQJkFwsbqKITEcVa8E+BFpxOf7s=
+github.com/decred/dcrd/database/v2 v2.0.2/go.mod h1:S78KbTCCJWUTJDVTByiQuB+HmL0DM2vIMsa2WsrF9KM=
 github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o=
 github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8=
-github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0 h1:E5KszxGgpjpmW8vN811G6rBAZg0/S/DftdGqN4FW5x4=
-github.com/decred/dcrd/dcrec/edwards/v2 v2.0.0/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc=
 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1 h1:V6eqU1crZzuoFT4KG2LhaU5xDSdkHuvLQsj25wd7Wb4=
 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.1/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc=
-github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY=
-github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY=
-github.com/decred/dcrd/dcrutil/v2 v2.0.1 h1:aL+c7o7Q66HV1gIif+XkNYo9DeorN3l01Vns8mh0mqs=
-github.com/decred/dcrd/dcrutil/v2 v2.0.1/go.mod h1:JdEgF6eh0TTohPeiqDxqDSikTSvAczq0J7tFMyyeD+k=
-github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM=
+github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw=
+github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
+github.com/decred/dcrd/dcrutil/v3 v3.0.0 h1:n6uQaTQynIhCY89XsoDk2WQqcUcnbD+zUM9rnZcIOZo=
+github.com/decred/dcrd/dcrutil/v3 v3.0.0/go.mod h1:iVsjcqVzLmYFGCZLet2H7Nq+7imV9tYcuY+0lC2mNsY=
+github.com/decred/dcrd/txscript/v3 v3.0.0 h1:74NmirXAIskbGP0g9OWtrmN7OxDbWJ9G73a5uoxTkcM=
+github.com/decred/dcrd/txscript/v3 v3.0.0/go.mod h1:pdvnlD4KGdDoc09cvWRJ8EoRQUaiUz41uDevOWuEfII=
 github.com/decred/dcrd/wire v1.4.0 h1:KmSo6eTQIvhXS0fLBQ/l7hG7QLcSJQKSwSyzSqJYDk0=
 github.com/decred/dcrd/wire v1.4.0/go.mod h1:WxC/0K+cCAnBh+SKsRjIX9YPgvrjhmE+6pZlel1G7Ro=
-github.com/decred/slog v1.0.0 h1:Dl+W8O6/JH6n2xIFN2p3DNjCmjYwvrXsjlSJTQQ4MhE=
-github.com/decred/slog v1.0.0/go.mod h1:zR98rEZHSnbZ4WHZtO0iqmSZjDLKhkXfrPTZQKtAonQ=
 github.com/decred/slog v1.1.0 h1:uz5ZFfmaexj1rEDgZvzQ7wjGkoSPjw2LCh8K+K1VrW4=
 github.com/decred/slog v1.1.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
-github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-golang.org/x/net v0.0.0-20180719180050-a680a1efc54d h1:i6RB+Qz1ug7TvJdY4zieRMpnLAtkHSHTOvApNXfLT4A=
-golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca h1:Ld/zXl5t4+D69SiV4JoN7kkfvJdOWlPpfxrzxpLMoUk=
+github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/go.mod b/go.mod
index 76ac2000..92f00c66 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/decred/base58 v1.0.3
 	github.com/decred/dcrd/addrmgr v1.1.0
 	github.com/decred/dcrd/bech32 v1.0.0
-	github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200921173733-67245079e9fb
+	github.com/decred/dcrd/blockchain/stake/v3 v3.0.0
 	github.com/decred/dcrd/blockchain/standalone/v2 v2.0.0
 	github.com/decred/dcrd/blockchain/v3 v3.0.0-20200215031403-6b2ce76f0986
 	github.com/decred/dcrd/certgen v1.1.0
@@ -16,7 +16,7 @@ require (
 	github.com/decred/dcrd/chaincfg/v3 v3.0.0
 	github.com/decred/dcrd/connmgr/v3 v3.0.0-20200215031403-6b2ce76f0986
 	github.com/decred/dcrd/crypto/ripemd160 v1.0.1
-	github.com/decred/dcrd/database/v2 v2.0.1
+	github.com/decred/dcrd/database/v2 v2.0.2
 	github.com/decred/dcrd/dcrec v1.0.0
 	github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0
 	github.com/decred/dcrd/dcrjson/v3 v3.1.0
diff --git a/internal/mempool/error.go b/internal/mempool/error.go
index 114df6a7..11186eab 100644
--- a/internal/mempool/error.go
+++ b/internal/mempool/error.go
@@ -12,116 +12,132 @@ import (
 	"github.com/decred/dcrd/blockchain/v3"
 )
 
-// RuleError identifies a rule violation.  It is used to indicate that
-// processing of a transaction failed due to one of the many validation
-// rules.  The caller can use type assertions to determine if a failure was
-// specifically due to a rule violation and use the Err field to access the
-// underlying error, which will be either a TxRuleError or a
-// blockchain.RuleError.
-type RuleError struct {
-	Err error
-}
+// ErrorKind identifies a kind of error.  It has full support for errors.Is and
+// errors.As, so the caller can directly check against an error kind when
+// determining the reason for an error.
+type ErrorKind string
 
-// Error satisfies the error interface and prints human-readable errors.
-func (e RuleError) Error() string {
-	if e.Err == nil {
-		return "<nil>"
-	}
-	return e.Err.Error()
-}
+const (
+	// ErrorInvalid indicates a mempool transaction is invalid per consensus.
+	ErrInvalid = ErrorKind("ErrInvalid")
 
-// ErrorCode identifies the kind of error.
-type ErrorCode int
+	// ErrorOrphanPolicyViolation indicates that an orphan block violates the
+	// prevailing orphan policy.
+	ErrOrphanPolicyViolation = ErrorKind("ErrOrphanPolicyViolation")
 
-const (
-	ErrOther ErrorCode = iota
-	ErrInvalid
-	ErrOrphanPolicyViolation
-	ErrMempoolDoubleSpend
-	ErrAlreadyVoted
-	ErrDuplicate
-	ErrCoinbase
-	ErrExpired
-	ErrNonStandard
-	ErrDustOutput
-	ErrInsufficientFee
-	ErrTooManyVotes
-	ErrDuplicateRevocation
-	ErrOldVote
-	ErrAlreadyExists
-	ErrSeqLockUnmet
-	ErrInsufficientPriority
-	ErrFeeTooHigh
-	ErrOrphan
-	ErrTooManyTSpends
-	ErrTSpendMinedOnAncestor
-	ErrTSpendInvalidExpiry
+	// ErrMempoolDoubleSpend indicates a transaction that attempts to to spend
+	// coins already spent by other transactions in the pool.
+	ErrMempoolDoubleSpend = ErrorKind("ErrMempoolDoubleSpend")
+
+	// ErrAlreadyVoted indicates a ticket already voted.
+	ErrAlreadyVoted = ErrorKind("ErrorAlreadyVoted")
+
+	// ErrDuplicate indicates a transaction already exists in the mempool.
+	ErrDuplicate = ErrorKind("ErrDuplicate")
+
+	// ErrCoinbase indicates a transaction is a standalone coinbase transaction.
+	ErrCoinbase = ErrorKind("ErrCoinbase")
+
+	// ErrExpired indicates a transaction will be expired as of the next block.
+	ErrExpired = ErrorKind("ErrExpired")
+
+	// ErrNonStandard indicates a non-standard transaction.
+	ErrNonStandard = ErrorKind("ErrNonStandard")
+
+	// ErrDustOutput indicates a transaction has one or more dust outputs.
+	ErrDustOutput = ErrorKind("ErrDustOutput")
+
+	// ErrInsufficientFee indicates a transaction cannot does not pay the minimum
+	// fee required by the active policy.
+	ErrInsufficientFee = ErrorKind("ErrInsufficientFee")
+
+	// ErrTooManyVotes indicates the number of vote double spends exceeds the
+	// maximum allowed.
+	ErrTooManyVotes = ErrorKind("ErrTooManyVotes")
+
+	// ErrDuplicateRevocation indicates a revocation already exists in the
+	// mempool.
+	ErrDuplicateRevocation = ErrorKind("ErrDuplicateRevocation")
+
+	// ErrOldVote indicates a ticket votes on a block height lower than
+	// the minimum allowed by the mempool.
+	ErrOldVote = ErrorKind("ErrOldVote")
+
+	// ErrAlreadyExists indicates a transaction already exists on the
+	// main chain and is not fully spent.
+	ErrAlreadyExists = ErrorKind("ErrAlreadyExists")
+
+	// ErrSeqLockUnmet indicates a transaction sequence locks are not active.
+	ErrSeqLockUnmet = ErrorKind("ErrSeqLockUnmet")
+
+	// ErrInsufficientPriority indicates a non-stake transaction has a
+	// priority lower than the minimum allowed.
+	ErrInsufficientPriority = ErrorKind("ErrInsufficientPriority")
+
+	// ErrFeeTooHigh indicates a transaction pays fees above the maximum
+	// allowed by the active policy.
+	ErrFeeTooHigh = ErrorKind("ErrFeeTooHigh")
+
+	// ErrOrphan indicates a transaction is an orphan.
+	ErrOrphan = ErrorKind("ErrOrphan")
+
+	// ErrTooManyTSpends indicates the number of treasury spend hashes exceeds
+	// the maximum allowed.
+	ErrTooManyTSpends = ErrorKind("ErrTooManyTSpends")
+
+	// ErrTSpendMinedOnAncestor indicates a referenced treasury spend was
+	// already mined on an ancestor block.
+	ErrTSpendMinedOnAncestor = ErrorKind("ErrTSpendMinedOnAncestor")
+
+	// ErrTSpendInvalidExpiry indicates a treasury spend expiry is invalid.
+	ErrTSpendInvalidExpiry = ErrorKind("ErrTSpendInvalidExpiry")
 )
 
-// TxRuleError identifies a rule violation.  It is used to indicate that
+// Error satisfies the error interface and prints human-readable errors.
+func (e ErrorKind) Error() string {
+	return string(e)
+}
+
+// RuleError identifies a rule violation.  It is used to indicate that
 // processing of a transaction failed due to one of the many validation
-// rules.  The caller can use type assertions to determine if a failure was
-// specifically due to a rule violation and access the ErrorCode field to
-// ascertain the specific reason for the rule violation.
-type TxRuleError struct {
-	// ErrorCode is the mempool package error code ID.
-	ErrorCode ErrorCode
-
-	// Description is an additional human readable description of the
-	// error.
+// rules.  It has full support for errors.Is and errors.As, so the caller
+// can ascertain the specific reason for the error by checking the
+// underlying error, which will be either an ErrorKind or blockchain.RuleError.
+type RuleError struct {
+	Err         error
 	Description string
 }
 
 // Error satisfies the error interface and prints human-readable errors.
-func (e TxRuleError) Error() string {
+func (e RuleError) Error() string {
 	return e.Description
 }
 
-// txRuleError creates an underlying TxRuleError with the given a set of
-// arguments and returns a RuleError that encapsulates it.
-func txRuleError(code ErrorCode, desc string) RuleError {
-	return RuleError{
-		Err: TxRuleError{ErrorCode: code, Description: desc},
-	}
+// Unwrap returns the underlying wrapped error.
+func (e RuleError) Unwrap() error {
+	return e.Err
 }
 
 // chainRuleError returns a RuleError that encapsulates the given
 // blockchain.RuleError.
 func chainRuleError(chainErr blockchain.RuleError) RuleError {
-	return RuleError{
-		Err: chainErr,
-	}
+	return RuleError{Err: chainErr, Description: chainErr.Description}
 }
 
-// IsErrorCode returns true if the passed error encodes a TxRuleError with the
-// given ErrorCode, either directly or embedded in an outer RuleError.
-func IsErrorCode(err error, code ErrorCode) bool {
-	// Unwrap RuleError if necessary.
-	var rerr RuleError
-	if errors.As(err, &rerr) {
-		err = rerr.Err
-	}
-
-	var trerr TxRuleError
-	return errors.As(err, &trerr) &&
-		trerr.ErrorCode == code
+// txRuleError creates a RuleError given a set of arguments.
+func txRuleError(kind ErrorKind, desc string) RuleError {
+	return RuleError{Err: kind, Description: desc}
 }
 
-// wrapTxRuleError returns a new RuleError with an underlying TxRuleError,
+// wrapTxRuleError returns a new RuleError with an underlying ErrorKind,
 // replacing the description with the provided one while retaining the error
-// code from the original error if it can be determined.
-func wrapTxRuleError(errorCode ErrorCode, desc string, err error) error {
-	// Unwrap the underlying error if err is a RuleError
-	var rerr RuleError
-	if errors.As(err, &rerr) {
-		err = rerr.Err
-	}
-
-	// Override the passed error code with the ones from the error if it is a
-	// TxRuleError.
-	var txerr TxRuleError
-	if errors.As(err, &txerr) {
-		errorCode = txerr.ErrorCode
+// kind from the original error if it can be determined.
+func wrapTxRuleError(kind ErrorKind, desc string, err error) error {
+	// Override the passed error kind with the one from the error if it is an
+	// ErrorKind.
+	var kerr ErrorKind
+	if errors.As(err, &kerr) {
+		kind = kerr
 	}
 
 	// Fill a default error description if empty.
@@ -129,5 +145,5 @@ func wrapTxRuleError(errorCode ErrorCode, desc string, err error) error {
 		desc = fmt.Sprintf("rejected: %v", err)
 	}
 
-	return txRuleError(errorCode, desc)
+	return txRuleError(kind, desc)
 }
diff --git a/internal/mempool/mempool.go b/internal/mempool/mempool.go
index b50eeaef..76c4dd13 100644
--- a/internal/mempool/mempool.go
+++ b/internal/mempool/mempool.go
@@ -222,6 +222,10 @@ type Policy struct {
 	//
 	// This function must be safe for concurrent access.
 	AcceptSequenceLocks func() (bool, error)
+
+	// EnableAncestorTracking controls whether the mining view tracks
+	// transaction relationships in the mempool.
+	EnableAncestorTracking bool
 }
 
 // TxDesc is a descriptor containing a transaction in the mempool along with
@@ -259,6 +263,31 @@ type orphanTx struct {
 	expiration time.Time
 }
 
+// txDescGraph relates a set of transactions to their respective descendants and
+// ancestors. It only stores transactions that have at least one edge relating
+// to another.
+type txDescGraph struct {
+	forEachRedeemer func(tx *dcrutil.Tx, f func(redeemerTx *mining.TxDesc))
+	childrenOf      map[chainhash.Hash]map[chainhash.Hash]*mining.TxDesc
+	parentsOf       map[chainhash.Hash]map[chainhash.Hash]*mining.TxDesc
+}
+
+// txDescFind is used to inject a transaction repository into the mining view.
+// This might either come from the mempool's outpoint map or from the mining
+// view itself.
+type txDescFind func(txHash *chainhash.Hash) *mining.TxDesc
+
+// txMiningView represents a snapshot of all transactions in mempool
+// as well as the hierarchy of transactions, as they may spend from one another
+// in the mempool.
+type txMiningView struct {
+	rejected           map[chainhash.Hash]struct{}
+	txGraph            *txDescGraph
+	txDescs            []*mining.TxDesc
+	trackAncestorStats bool
+	ancestorStats      map[chainhash.Hash]*mining.TxAncestorStats
+}
+
 // TxPool is used as a source of transactions that need to be mined into blocks
 // and relayed to other peers.  It is safe for concurrent access from multiple
 // peers.
@@ -273,6 +302,7 @@ type TxPool struct {
 	orphans       map[chainhash.Hash]*orphanTx
 	orphansByPrev map[wire.OutPoint]map[chainhash.Hash]*dcrutil.Tx
 	outpoints     map[wire.OutPoint]*dcrutil.Tx
+	miningView    *txMiningView
 
 	staged          map[chainhash.Hash]*dcrutil.Tx
 	stagedOutpoints map[wire.OutPoint]*dcrutil.Tx
@@ -826,6 +856,100 @@ func (mp *TxPool) HaveAllTransactions(hashes []chainhash.Hash) bool {
 	return inPool
 }
 
+// forEachDescendant iterates depth-first over all transactions that depend on
+// the provided transaction hash and invokes function f with each in post-order.
+func (g *txDescGraph) forEachDescendant(txHash *chainhash.Hash,
+	seen map[chainhash.Hash]struct{}, f func(*mining.TxDesc)) {
+
+	for child, childDesc := range g.childrenOf[*txHash] {
+		if _, saw := seen[child]; saw {
+			continue
+		}
+
+		seen[child] = struct{}{}
+		g.forEachDescendant(&child, seen, f)
+		f(childDesc)
+	}
+}
+
+// updateStatsDescendantsRemoved removes the provided transaction's stats
+// from all descendant transactions.
+func (mv *txMiningView) updateStatsDescendantsRemoved(txDesc *mining.TxDesc) {
+	txHash := txDesc.Tx.Hash()
+	txSize := int64(txDesc.Tx.MsgTx().SerializeSize())
+	seen := make(map[chainhash.Hash]struct{})
+	mv.txGraph.forEachDescendant(txHash, seen, func(descendant *mining.TxDesc) {
+		descendantStats := mv.ancestorStats[*descendant.Tx.Hash()]
+		descendantStats.Fees -= txDesc.Fee
+		descendantStats.SizeBytes -= txSize
+		descendantStats.TotalSigOps -= txDesc.TotalSigOps
+		descendantStats.NumAncestors--
+	})
+}
+
+// remove deletes the provided txn hash from the graph. If it is the only
+// relationship held by a related transaction in the graph, that related
+// transaction is also removed.
+func (g *txDescGraph) remove(txHash *chainhash.Hash) {
+	// Remove references to tx from all children.
+	for childHash := range g.childrenOf[*txHash] {
+		delete(g.parentsOf[childHash], *txHash)
+
+		// If the child has no more parents, remove reference.
+		if len(g.parentsOf[childHash]) == 0 {
+			delete(g.parentsOf, childHash)
+		}
+	}
+
+	// Remove references to tx from all parents.
+	for parentHash := range g.parentsOf[*txHash] {
+		delete(g.childrenOf[parentHash], *txHash)
+
+		// If the parent has no more children, remove reference.
+		if len(g.childrenOf[parentHash]) == 0 {
+			delete(g.childrenOf, parentHash)
+		}
+	}
+
+	// Remove reference since we don't need to track this txn's
+	// parents or children
+	delete(g.parentsOf, *txHash)
+	delete(g.childrenOf, *txHash)
+}
+
+// find returns the TxDesc stored in the graph by its hash. If the transaction
+// is not in the graph, then a nil pointer is returned. Since each transaction
+// in the graph must have at least one edge connecting it to another
+// transaction, scanning for a known hash as a child or parent is sufficient
+// to recover its pointer.
+func (g *txDescGraph) find(txHash *chainhash.Hash) *mining.TxDesc {
+	for parentHash := range g.parentsOf[*txHash] {
+		return g.childrenOf[parentHash][*txHash]
+	}
+
+	for childHash := range g.childrenOf[*txHash] {
+		return g.parentsOf[childHash][*txHash]
+	}
+
+	return nil
+}
+
+// Remove stops tracking the transaction in the miningView,
+// if it exists. If updateDescendantStats is true, then the
+// statistics for all descendant transactions are updated
+// to account for the removal of an ancestor.
+func (mv *txMiningView) Remove(txHash *chainhash.Hash, updateDescendantStats bool) {
+	if mv.trackAncestorStats && updateDescendantStats {
+		txDesc := mv.txGraph.find(txHash)
+		if txDesc != nil {
+			mv.updateStatsDescendantsRemoved(txDesc)
+		}
+	}
+
+	mv.txGraph.remove(txHash)
+	delete(mv.ancestorStats, *txHash)
+}
+
 // removeTransaction is the internal function which implements the public
 // RemoveTransaction.  See the comment for RemoveTransaction for more details.
 //
@@ -869,7 +993,15 @@ func (mp *TxPool) removeTransaction(tx *dcrutil.Tx, removeRedeemers bool, isTrea
 		for _, txIn := range txDesc.Tx.MsgTx().TxIn {
 			delete(mp.outpoints, txIn.PreviousOutPoint)
 		}
+
+		// Stop tracking this transaction in the mining view.
+		// If redeeming transactions are going to be removed from the
+		// graph, then do not update their stats.
+		updateDescendantStats := !removeRedeemers
+		mp.miningView.Remove(tx.Hash(), updateDescendantStats)
+
 		delete(mp.pool, *txHash)
+
 		atomic.StoreInt64(&mp.lastUpdated, time.Now().Unix())
 
 		// Inform associated fee estimator that the transaction has been removed
@@ -924,12 +1056,149 @@ func (mp *TxPool) RemoveDoubleSpends(tx *dcrutil.Tx, isTreasuryEnabled bool) {
 	mp.mtx.Unlock()
 }
 
+// forEachAncestor iterates over all transactions in the graph that txHash
+// depends on and invokes the function f for each transaction,
+// in topological order.
+func (g *txDescGraph) forEachAncestor(txHash *chainhash.Hash,
+	seen map[chainhash.Hash]struct{}, f func(tx *mining.TxDesc)) {
+
+	for parent, parentDesc := range g.parentsOf[*txHash] {
+		if _, saw := seen[parent]; saw {
+			continue
+		}
+
+		seen[parent] = struct{}{}
+		g.forEachAncestor(&parent, seen, f)
+		f(parentDesc)
+	}
+}
+
+// aggregateAncestorStats adds the provided ancestor transaction's statistics
+// to the provided ancestor stats reference.
+func aggregateAncestorStats(stats *mining.TxAncestorStats, ancestor *mining.TxDesc) {
+	stats.NumAncestors++
+	stats.Fees += ancestor.Fee
+	stats.SizeBytes += int64(ancestor.Tx.MsgTx().SerializeSize())
+	stats.TotalSigOps += ancestor.TotalSigOps
+}
+
+// updateBundleStats ensures that the bundle stats for a given
+// transaction are up to date. It aggregates statistics for the provided
+// transaction based on its ancestors.
+func (mv *txMiningView) updateBundleStats(txDesc *mining.TxDesc) {
+	seen := make(map[chainhash.Hash]struct{})
+	txHash := txDesc.Tx.Hash()
+	stats := &mining.TxAncestorStats{}
+	mv.txGraph.forEachAncestor(txHash, seen, func(txDesc *mining.TxDesc) {
+		aggregateAncestorStats(stats, txDesc)
+	})
+
+	mv.ancestorStats[*txDesc.Tx.Hash()] = stats
+}
+
+// findTx returns a transaction from the mempool by hash.  If it does not exist
+// in the mempool, a nil pointer is returned.
+func (mp *TxPool) findTx(txHash *chainhash.Hash) *mining.TxDesc {
+	poolTx := mp.pool[*txHash]
+	if poolTx == nil {
+		return nil
+	}
+
+	return &poolTx.TxDesc
+}
+
+// addChild adds a child transaction to the graph as a dependent of the
+// provided transaction tx.
+func (g *txDescGraph) addChild(tx *mining.TxDesc, child *mining.TxDesc) {
+	txHash := *tx.Tx.Hash()
+	if _, exists := g.childrenOf[txHash]; !exists {
+		g.childrenOf[txHash] = make(map[chainhash.Hash]*mining.TxDesc)
+	}
+
+	g.childrenOf[txHash][*child.Tx.Hash()] = child
+}
+
+// addParent adds a parent transaction to the graph as a dependency of the
+// provided transaction tx.
+func (g *txDescGraph) addParent(tx *mining.TxDesc, parent *mining.TxDesc) {
+	txHash := *tx.Tx.Hash()
+	if _, exists := g.parentsOf[txHash]; !exists {
+		g.parentsOf[txHash] = make(map[chainhash.Hash]*mining.TxDesc)
+	}
+
+	g.parentsOf[txHash][*parent.Tx.Hash()] = parent
+}
+
+// When a transaction is added to the mempool, create a 2-way association
+// between itself and its parents in the mempool.
+func (g *txDescGraph) insert(txDesc *mining.TxDesc, findTx txDescFind) {
+	seen := make(map[chainhash.Hash]struct{})
+
+	// Fetch transactions that spend this one from the graph.
+	g.forEachRedeemer(txDesc.Tx, func(child *mining.TxDesc) {
+		g.addChild(txDesc, child)
+		g.addParent(child, txDesc)
+	})
+
+	// Relate self with direct ancestors.
+	for _, txIn := range txDesc.Tx.MsgTx().TxIn {
+		parentHash := txIn.PreviousOutPoint.Hash
+		if _, saw := seen[parentHash]; saw {
+			continue
+		}
+
+		seen[parentHash] = struct{}{}
+
+		// Find parents from the mempool and add to graph.
+		if parentTx := findTx(&parentHash); parentTx != nil {
+			g.addParent(txDesc, parentTx)
+			g.addChild(parentTx, txDesc)
+		}
+	}
+}
+
+// updateStatsDescendantsAdded adds the provided transaction's stats
+// to all transactions in the graph that depend on it.
+func (mv *txMiningView) updateStatsDescendantsAdded(txDesc *mining.TxDesc) {
+	txHash := txDesc.Tx.Hash()
+	txSize := int64(txDesc.Tx.MsgTx().SerializeSize())
+	seen := make(map[chainhash.Hash]struct{})
+	mv.txGraph.forEachDescendant(txHash, seen, func(descendant *mining.TxDesc) {
+		descendantStats := mv.ancestorStats[*descendant.Tx.Hash()]
+		descendantStats.Fees += txDesc.Fee
+		descendantStats.SizeBytes += txSize
+		descendantStats.TotalSigOps += txDesc.TotalSigOps
+		descendantStats.NumAncestors++
+	})
+}
+
+// addTransaction inserts a TxDesc into the mining view if it has a parent
+// or child in the view, or has a relationship with another transaction through
+// findTx.
+func (mv *txMiningView) addTransaction(txDesc *mining.TxDesc, findTx txDescFind) {
+	// Track the txn in the graph and attempt to relate it to at least one other
+	// transaction.
+	mv.txGraph.insert(txDesc, findTx)
+
+	if mv.trackAncestorStats {
+		// Ensure that the statistics for this transaction are up to date
+		// given the current ancestors in the view.
+		mv.updateBundleStats(txDesc)
+
+		// If a transaction is added back to the view,
+		// reconnect it with transactions already in the view.
+		// This is necessary to account for reorgs.
+		mv.updateStatsDescendantsAdded(txDesc)
+	}
+}
+
 // addTransaction adds the passed transaction to the memory pool.  It should
 // not be called directly as it doesn't perform any validation.  This is a
 // helper for maybeAcceptTransaction.
 //
 // This function MUST be called with the mempool lock held (for writes).
-func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *dcrutil.Tx, txType stake.TxType, height int64, fee int64, isTreasuryEnabled bool) {
+func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *dcrutil.Tx,
+	txType stake.TxType, height int64, fee int64, isTreasuryEnabled bool, totalSigOps int) {
 
 	// Notify callback about vote if requested.
 	if mp.cfg.OnVoteReceived != nil && txType == stake.TxTypeSSGen {
@@ -939,16 +1208,21 @@ func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *dcrutil
 	// Add the transaction to the pool and mark the referenced outpoints
 	// as spent by the pool.
 	msgTx := tx.MsgTx()
-	mp.pool[*tx.Hash()] = &TxDesc{
+	poolTxDesc := &TxDesc{
 		TxDesc: mining.TxDesc{
-			Tx:     tx,
-			Type:   txType,
-			Added:  time.Now(),
-			Height: height,
-			Fee:    fee,
+			Tx:          tx,
+			Type:        txType,
+			Added:       time.Now(),
+			Height:      height,
+			Fee:         fee,
+			TotalSigOps: totalSigOps,
 		},
 		StartingPriority: mining.CalcPriority(msgTx, utxoView, height),
 	}
+
+	mp.pool[*tx.Hash()] = poolTxDesc
+	mp.miningView.addTransaction(&poolTxDesc.TxDesc, mp.findTx)
+
 	for _, txIn := range msgTx.TxIn {
 		mp.outpoints[txIn.PreviousOutPoint] = tx
 	}
@@ -1471,7 +1745,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
 	// the coinbase address itself can contain signature operations, the
 	// maximum allowed signature operations per transaction is less than
 	// the maximum allowed signature operations per block.
-	numSigOps, err := blockchain.CountP2SHSigOps(tx, false,
+	numP2SHSigOps, err := blockchain.CountP2SHSigOps(tx, false,
 		(txType == stake.TxTypeSSGen), utxoView, isTreasuryEnabled)
 	if err != nil {
 		var cerr blockchain.RuleError
@@ -1481,10 +1755,11 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
 		return nil, err
 	}
 
-	numSigOps += blockchain.CountSigOps(tx, false, isVote, isTreasuryEnabled)
-	if numSigOps > mp.cfg.Policy.MaxSigOpsPerTx {
+	numSigOps := blockchain.CountSigOps(tx, false, isVote, isTreasuryEnabled)
+	totalSigOps := numP2SHSigOps + numSigOps
+	if totalSigOps > mp.cfg.Policy.MaxSigOpsPerTx {
 		str := fmt.Sprintf("transaction %v has too many sigops: %d > %d",
-			txHash, numSigOps, mp.cfg.Policy.MaxSigOpsPerTx)
+			txHash, totalSigOps, mp.cfg.Policy.MaxSigOpsPerTx)
 		return nil, txRuleError(ErrNonStandard, str)
 	}
 
@@ -1694,7 +1969,8 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
 	}
 
 	// Add to transaction pool.
-	mp.addTransaction(utxoView, tx, txType, bestHeight, txFee, isTreasuryEnabled)
+	mp.addTransaction(utxoView, tx, txType, bestHeight, txFee, isTreasuryEnabled,
+		totalSigOps)
 
 	// A regular transaction that is added back to the mempool causes
 	// any mempool tickets that redeem it to leave the main pool and enter the
@@ -2118,20 +2394,17 @@ func (mp *TxPool) VerboseTxDescs() []*VerboseTxDesc {
 	return result
 }
 
-// MiningDescs returns a slice of mining descriptors for all the transactions
+// miningDescs returns a slice of mining descriptors for all transactions
 // in the pool.
 //
-// This is part of the mining.TxSource interface implementation and is safe for
-// concurrent access as required by the interface contract.
-func (mp *TxPool) MiningDescs() []*mining.TxDesc {
-	mp.mtx.RLock()
+// This function MUST be called with the mempool lock held (for reads).
+func (mp *TxPool) miningDescs() []*mining.TxDesc {
 	descs := make([]*mining.TxDesc, len(mp.pool))
 	i := 0
 	for _, desc := range mp.pool {
 		descs[i] = &desc.TxDesc
 		i++
 	}
-	mp.mtx.RUnlock()
 
 	return descs
 }
@@ -2144,10 +2417,213 @@ func (mp *TxPool) LastUpdated() time.Time {
 	return time.Unix(atomic.LoadInt64(&mp.lastUpdated), 0)
 }
 
+// newTxDescGraph returns a new instance of txDescGraph. The graph is provided
+// with a wrapper over the mempool's outpoint map to determine which
+// transactions in the pool spend from ones newly added to the graph.
+func (mp *TxPool) newTxDescGraph() *txDescGraph {
+	// for a given transaction, scan the mempool to find which transactions
+	// spend it.
+	forEachRedeemer := func(tx *dcrutil.Tx, f func(redeemerTx *mining.TxDesc)) {
+		prevOut := wire.OutPoint{Hash: *tx.Hash(), Tree: tx.Tree()}
+		txOutLen := uint32(len(tx.MsgTx().TxOut))
+		for i := uint32(0); i < txOutLen; i++ {
+			prevOut.Index = i
+			if txRedeemer, exists := mp.outpoints[prevOut]; exists {
+				f(&mp.pool[txRedeemer.MsgTx().TxHash()].TxDesc)
+			}
+		}
+	}
+
+	return &txDescGraph{
+		forEachRedeemer: forEachRedeemer,
+		childrenOf:      make(map[chainhash.Hash]map[chainhash.Hash]*mining.TxDesc),
+		parentsOf:       make(map[chainhash.Hash]map[chainhash.Hash]*mining.TxDesc),
+	}
+}
+
+// TxDescs returns a collection of all transactions available in the view.
+func (mv *txMiningView) TxDescs() []*mining.TxDesc {
+	return mv.txDescs
+}
+
+// defaultAncestorStats is the default-state of ancestor stats for a transaction
+// that has no ancestors.
+var defaultAncestorStats = &mining.TxAncestorStats{}
+
+// AncestorStats returns the view's cached statistics for all of the provided
+// transaction's ancestors.
+func (mv *txMiningView) AncestorStats(txHash *chainhash.Hash) (*mining.TxAncestorStats, bool) {
+	if !mv.trackAncestorStats {
+		return defaultAncestorStats, false
+	}
+
+	if stats, exists := mv.ancestorStats[*txHash]; exists {
+		return stats, exists
+	}
+
+	return defaultAncestorStats, false
+}
+
+// Ancestors returns a collection of all transactions in the graph that the
+// provided transaction hash depends on, and its ancestors' bundle stats.
+func (mv *txMiningView) Ancestors(txHash *chainhash.Hash) []*mining.TxDesc {
+	var ancestors []*mining.TxDesc
+	if !mv.trackAncestorStats {
+		return nil
+	}
+
+	var stats *mining.TxAncestorStats
+	seen := make(map[chainhash.Hash]struct{})
+	mv.txGraph.forEachAncestor(txHash, seen, func(txDesc *mining.TxDesc) {
+		if stats == nil {
+			stats = &mining.TxAncestorStats{}
+		}
+
+		aggregateAncestorStats(stats, txDesc)
+		ancestors = append(ancestors, txDesc)
+	})
+
+	if stats == nil {
+		delete(mv.ancestorStats, *txHash)
+	} else {
+		// Update the cache.
+		mv.ancestorStats[*txHash] = stats
+	}
+
+	return ancestors
+}
+
+// HasParents returns true if the provided transaction hash spends from another
+// transaction in the mining view.
+func (mv *txMiningView) HasParents(txHash *chainhash.Hash) bool {
+	return len(mv.txGraph.parentsOf[*txHash]) > 0
+}
+
+// Parents returns a set of transactions in the graph that the provided
+// transaction hash spends from in the view. The order of elements
+// returned is not guaranteed.
+func (mv *txMiningView) Parents(txHash *chainhash.Hash) []*mining.TxDesc {
+	parentsOf := mv.txGraph.parentsOf[*txHash]
+	if len(parentsOf) == 0 {
+		return nil
+	}
+
+	parents := make([]*mining.TxDesc, 0, len(parentsOf))
+	for _, tx := range parentsOf {
+		parents = append(parents, tx)
+	}
+
+	return parents
+}
+
+// Children returns a set of transactions in the graph that spend
+// from the provided transaction hash. The order of elements
+// returned is not guaranteed.
+func (mv *txMiningView) Children(txHash *chainhash.Hash) []*mining.TxDesc {
+	childrenOf := mv.txGraph.childrenOf[*txHash]
+	if len(childrenOf) == 0 {
+		return nil
+	}
+
+	children := make([]*mining.TxDesc, 0, len(childrenOf))
+	for _, tx := range childrenOf {
+		children = append(children, tx)
+	}
+
+	return children
+}
+
+// Reject stops tracking the transaction in the view, if it exists, and all
+// of its descendants.  Also flags the provided transaction as rejected and
+// tracks the hash as rejected in this instance of the mining view. Rejected
+// transactions are not shared between mining view instances, and a new
+// mining view is always initialized with an empty set of rejected transactions.
+func (mv *txMiningView) Reject(txHash *chainhash.Hash) {
+	seen := make(map[chainhash.Hash]struct{})
+	mv.txGraph.forEachDescendant(txHash, seen, func(descendant *mining.TxDesc) {
+		mv.Remove(descendant.Tx.Hash(), false)
+		mv.rejected[*descendant.Tx.Hash()] = struct{}{}
+	})
+
+	mv.Remove(txHash, false)
+	mv.rejected[*txHash] = struct{}{}
+}
+
+// IsRejected returns true if the given hash has been passed to Reject
+// on this instance of the view.
+func (mv *txMiningView) IsRejected(txHash *chainhash.Hash) bool {
+	_, exist := mv.rejected[*txHash]
+	return exist
+}
+
+// clone returns a copy of the current graph.
+func (g *txDescGraph) clone(fetchTx txDescFind) *txDescGraph {
+	graph := &txDescGraph{
+		parentsOf: make(map[chainhash.Hash]map[chainhash.Hash]*mining.TxDesc,
+			len(g.parentsOf)),
+		childrenOf: make(map[chainhash.Hash]map[chainhash.Hash]*mining.TxDesc,
+			len(g.childrenOf)),
+	}
+
+	// Source transactions from within the graph to decouple the cloned
+	// mining view instance from the mempool.
+	graph.forEachRedeemer = func(tx *dcrutil.Tx, f func(
+		redeemerTx *mining.TxDesc)) {
+		for _, childTx := range graph.childrenOf[*tx.Hash()] {
+			f(childTx)
+		}
+	}
+
+	// Copy parents and children. Anything tracked by the graph
+	// will either be a child or parent of another element in the graph.
+	for txHash := range g.parentsOf {
+		txDesc := fetchTx(&txHash)
+		graph.insert(txDesc, fetchTx)
+	}
+
+	for txHash := range g.childrenOf {
+		txDesc := fetchTx(&txHash)
+		graph.insert(txDesc, fetchTx)
+	}
+
+	return graph
+}
+
+// MiningView returns a slice of mining descriptors for all the transactions
+// in the pool in addition to a snapshot of the current pool's transaction
+// relationships.
+//
+// This is part of the mining.TxSource interface implementation and is safe for
+// concurrent access as required by the interface contract.
+func (mp *TxPool) MiningView() mining.TxMiningView {
+	mp.mtx.RLock()
+
+	view := &txMiningView{
+		rejected:           make(map[chainhash.Hash]struct{}),
+		txGraph:            mp.miningView.txGraph.clone(mp.findTx),
+		txDescs:            mp.miningDescs(),
+		trackAncestorStats: mp.miningView.trackAncestorStats,
+		ancestorStats: make(map[chainhash.Hash]*mining.TxAncestorStats,
+			len(mp.miningView.ancestorStats)),
+	}
+
+	for key, value := range mp.miningView.ancestorStats {
+		view.ancestorStats[key] = &mining.TxAncestorStats{
+			Fees:         value.Fees,
+			SizeBytes:    value.SizeBytes,
+			TotalSigOps:  value.TotalSigOps,
+			NumAncestors: value.NumAncestors,
+		}
+	}
+
+	mp.mtx.RUnlock()
+	return view
+}
+
 // New returns a new memory pool for validating and storing standalone
 // transactions until they are mined into a block.
 func New(cfg *Config) *TxPool {
-	return &TxPool{
+	mp := &TxPool{
 		cfg:             *cfg,
 		pool:            make(map[chainhash.Hash]*TxDesc),
 		orphans:         make(map[chainhash.Hash]*orphanTx),
@@ -2159,4 +2635,14 @@ func New(cfg *Config) *TxPool {
 		staged:          make(map[chainhash.Hash]*dcrutil.Tx),
 		stagedOutpoints: make(map[wire.OutPoint]*dcrutil.Tx),
 	}
+
+	mp.miningView = &txMiningView{
+		rejected:           make(map[chainhash.Hash]struct{}),
+		txGraph:            mp.newTxDescGraph(),
+		txDescs:            nil,
+		trackAncestorStats: cfg.Policy.EnableAncestorTracking,
+		ancestorStats:      make(map[chainhash.Hash]*mining.TxAncestorStats),
+	}
+
+	return mp
 }
diff --git a/internal/mining/cpuminer/cpuminer.go b/internal/mining/cpuminer/cpuminer.go
index 4bcc6cb3..9a251f7e 100644
--- a/internal/mining/cpuminer/cpuminer.go
+++ b/internal/mining/cpuminer/cpuminer.go
@@ -218,7 +218,7 @@ func (m *CPUMiner) submitBlock(block *dcrutil.Block) bool {
 		// debug since it is expected to happen from time to time and not really
 		// an error.
 		if m.cfg.ChainParams.ReduceMinDifficulty &&
-			rErr.ErrorCode == blockchain.ErrHighHash {
+			errors.Is(rErr, blockchain.ErrHighHash) {
 			log.Debugf("Block submitted via CPU miner rejected because of "+
 				"ReduceMinDifficulty time sync failure: %v", err)
 			return false
diff --git a/internal/mining/mining.go b/internal/mining/mining.go
index a4996794..a6219973 100644
--- a/internal/mining/mining.go
+++ b/internal/mining/mining.go
@@ -62,6 +62,73 @@ type TxDesc struct {
 
 	// Fee is the total fee the transaction associated with the entry pays.
 	Fee int64
+
+	// TotalSigOps is the total signature operations for this transaction.
+	TotalSigOps int
+}
+
+// TxAncestorStats is a descriptor that stores aggregated statistics for the
+// unconfirmed ancestors of a transasction.
+type TxAncestorStats struct {
+	// Fees is the sum of all fees of unconfirmed ancestors.
+	Fees int64
+
+	// SizeBytes is the total size of all unconfirmed ancestors.
+	SizeBytes int64
+
+	// TotalSigOps is the total number of signature operations of all ancestors.
+	TotalSigOps int
+
+	// NumAncestors is the total number of ancestors for a given transaction.
+	NumAncestors int
+}
+
+// TxMiningView is a snapshot of the tx source.
+type TxMiningView interface {
+	// TxDescs returns a slice of mining descriptors for all minable
+	// transactions in the source pool.
+	TxDescs() []*TxDesc
+
+	// AncestorStats returns the last known ancestor stats for the provided
+	// transaction hash, and a boolean indicating whether ancestors are being
+	// tracked for it.
+	//
+	// Calling Ancestors will update the value returned by this function to
+	// reflect the newly calculated statistics for those ancestors.
+	AncestorStats(txHash *chainhash.Hash) (*TxAncestorStats, bool)
+
+	// Ancestors returns all transactions in the mining view that the provided
+	// transaction hash depends on.
+	Ancestors(txHash *chainhash.Hash) []*TxDesc
+
+	// HasParents returns true if the provided transaction hash has any
+	// ancestors known to the view.
+	HasParents(txHash *chainhash.Hash) bool
+
+	// Parents returns a set of transactions in the graph that the provided
+	// transaction hash spends from in the view. The order of elements
+	// returned is not guaranteed.
+	Parents(txHash *chainhash.Hash) []*TxDesc
+
+	// Children returns a set of transactions in the graph that spend
+	// from the provided transaction hash. The order of elements
+	// returned is not guaranteed.
+	Children(txHash *chainhash.Hash) []*TxDesc
+
+	// Remove causes the provided transaction to be removed from the view, if
+	// it exists. The updateDescendantStats parameter indicates whether the
+	// descendent transactions of the provided txHash should have their ancestor
+	// stats updated within the view to account for the removal of this
+	// transaction.
+	Remove(txHash *chainhash.Hash, updateDescendantStats bool)
+
+	// Reject removes and flags the provided transaction hash and all of its
+	// descendants in the view as rejected.
+	Reject(txHash *chainhash.Hash)
+
+	// IsRejected checks to see if a transaction that once existed in the view
+	// has been rejected.
+	IsRejected(txHash *chainhash.Hash) bool
 }
 
 // VoteDesc is a descriptor about a vote transaction in a transaction source
@@ -82,10 +149,6 @@ type TxSource interface {
 	// removed from the source pool.
 	LastUpdated() time.Time
 
-	// MiningDescs returns a slice of mining descriptors for all the
-	// transactions in the source pool.
-	MiningDescs() []*TxDesc
-
 	// HaveTransaction returns whether or not the passed transaction hash
 	// exists in the source pool.
 	HaveTransaction(hash *chainhash.Hash) bool
@@ -108,6 +171,9 @@ type TxSource interface {
 	// known to be disapproved according to the votes currently in the
 	// source pool.
 	IsRegTxTreeKnownDisapproved(hash *chainhash.Hash) bool
+
+	// MiningView returns a snapshot of the underlying TxSource.
+	MiningView() TxMiningView
 }
 
 const (
@@ -154,17 +220,11 @@ const (
 // transaction to be prioritized and track dependencies on other transactions
 // which have not been mined into a block yet.
 type txPrioItem struct {
-	tx       *dcrutil.Tx
+	txDesc   *TxDesc
 	txType   stake.TxType
 	fee      int64
 	priority float64
 	feePerKB float64
-
-	// dependsOn holds a map of transaction hashes which this one depends
-	// on.  It will only be set when the transaction references other
-	// transactions in the source pool and hence must come after them in
-	// a block.
-	dependsOn map[chainhash.Hash]struct{}
 }
 
 // txPriorityQueueLessFunc describes a function that can be used as a compare
@@ -762,14 +822,14 @@ func spendTransaction(utxoView *blockchain.UtxoViewpoint, tx *dcrutil.Tx, height
 
 // logSkippedDeps logs any dependencies which are also skipped as a result of
 // skipping a transaction while generating a block template at the trace level.
-func logSkippedDeps(tx *dcrutil.Tx, deps map[chainhash.Hash]*txPrioItem) {
+func logSkippedDeps(tx *dcrutil.Tx, deps []*TxDesc) {
 	if deps == nil {
 		return
 	}
 
 	for _, item := range deps {
 		log.Tracef("Skipping tx %s since it depends on %s\n",
-			item.tx.Hash(), tx.Hash())
+			item.Tx.Hash(), tx.Hash())
 	}
 }
 
@@ -1039,6 +1099,14 @@ func NewBlkTmplGenerator(policy *Policy, txSource TxSource,
 	}
 }
 
+// calcFeePerKb returns an adjusted fee per kilobyte taking the provided
+// transaction and its ancestors into account.
+func calcFeePerKb(txDesc *TxDesc, ancestorStats *TxAncestorStats) float64 {
+	txSize := txDesc.Tx.MsgTx().SerializeSize()
+	return (float64(txDesc.Fee+ancestorStats.Fees) * float64(kilobyte)) /
+		float64(int64(txSize)+ancestorStats.SizeBytes)
+}
+
 // NewBlockTemplate returns a new block template that is ready to be solved
 // using the transactions from the passed transaction source pool and a coinbase
 // that either pays to the passed address if it is not nil, or a coinbase that
@@ -1235,13 +1303,15 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress dcrutil.Address) (*Bloc
 	// number of items that are available for the priority queue.  Also,
 	// choose the initial sort order for the priority queue based on whether
 	// or not there is an area allocated for high-priority transactions.
-	sourceTxns := g.txSource.MiningDescs()
+	miningView := g.txSource.MiningView()
+	sourceTxns := miningView.TxDescs()
 	sortedByFee := g.policy.BlockPrioritySize == 0
 	lessFunc := txPQByStakeAndFeeAndThenPriority
 	if sortedByFee {
 		lessFunc = txPQByStakeAndFee
 	}
 	priorityQueue := newTxPriorityQueue(len(sourceTxns), lessFunc)
+	prioritizedTxns := make(map[chainhash.Hash]struct{}, len(sourceTxns))
 
 	// Create a slice to hold the transactions to be included in the
 	// generated block with reserved space.  Also create a utxo view to
@@ -1250,13 +1320,6 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress dcrutil.Address) (*Bloc
 	blockTxns := make([]*dcrutil.Tx, 0, len(sourceTxns))
 	blockUtxos := blockchain.NewUtxoViewpoint(g.chain)
 
-	// dependers is used to track transactions which depend on another
-	// transaction in the source pool.  This, in conjunction with the
-	// dependsOn map kept with each dependent transaction helps quickly
-	// determine which dependent transactions are now eligible for inclusion
-	// in the block once each transaction has been included.
-	dependers := make(map[chainhash.Hash]map[chainhash.Hash]*txPrioItem)
-
 	// Create slices to hold the fees and number of signature operations
 	// for each of the selected transactions and add an entry for the
 	// coinbase.  This allows the code below to simply append details about
@@ -1273,6 +1336,11 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress dcrutil.Address) (*Bloc
 		len(sourceTxns))
 	knownDisapproved := g.txSource.IsRegTxTreeKnownDisapproved(&prevHash)
 
+	// Tracks the total number of transactions that depend on another
+	// from the tx source.
+	totalDescendantTxns := 0
+	prioItemMap := make(map[chainhash.Hash]*txPrioItem, len(sourceTxns))
+
 mempoolLoop:
 	for _, txDesc := range sourceTxns {
 		// A block can't have more than one coinbase or contain
@@ -1308,9 +1376,6 @@ mempoolLoop:
 		}
 
 		// Fetch all of the utxos referenced by the this transaction.
-		// NOTE: This intentionally does not fetch inputs from the
-		// mempool since a transaction which depends on other
-		// transactions in the mempool must come after those
 		utxos, err := g.chain.FetchUtxoView(tx, !knownDisapproved)
 		if err != nil {
 			log.Warnf("Unable to fetch utxo view for tx %s: "+
@@ -1321,7 +1386,7 @@ mempoolLoop:
 		// Setup dependencies for any transactions which reference
 		// other transactions in the mempool so they can be properly
 		// ordered below.
-		prioItem := &txPrioItem{tx: txDesc.Tx, txType: txDesc.Type}
+		prioItem := &txPrioItem{txDesc: txDesc, txType: txDesc.Type}
 		for i, txIn := range tx.MsgTx().TxIn {
 			// Evaluate if this is a stakebase input or not. If it is, continue
 			// without evaluation of the input.
@@ -1341,25 +1406,6 @@ mempoolLoop:
 						tx.Hash(), txIn.PreviousOutPoint)
 					continue mempoolLoop
 				}
-
-				// The transaction is referencing another
-				// transaction in the source pool, so setup an
-				// ordering dependency.
-				deps, exists := dependers[*originHash]
-				if !exists {
-					deps = make(map[chainhash.Hash]*txPrioItem)
-					dependers[*originHash] = deps
-				}
-				deps[*prioItem.tx.Hash()] = prioItem
-				if prioItem.dependsOn == nil {
-					prioItem.dependsOn = make(
-						map[chainhash.Hash]struct{})
-				}
-				prioItem.dependsOn[*originHash] = struct{}{}
-
-				// Skip the check below. We already know the
-				// referenced transaction is available.
-				continue
 			}
 		}
 
@@ -1374,15 +1420,21 @@ mempoolLoop:
 		// during calcMinRelayFee which rounds up to the nearest full
 		// kilobyte boundary.  This is beneficial since it provides an
 		// incentive to create smaller transactions.
-		txSize := tx.MsgTx().SerializeSize()
-		prioItem.feePerKB = (float64(txDesc.Fee) * float64(kilobyte)) /
-			float64(txSize)
-		prioItem.fee = txDesc.Fee
-
-		// Add the transaction to the priority queue to mark it ready
-		// for inclusion in the block unless it has dependencies.
-		if prioItem.dependsOn == nil {
+		ancestorStats, hasStats := miningView.AncestorStats(tx.Hash())
+		prioItem.feePerKB = calcFeePerKb(txDesc, ancestorStats)
+		prioItem.fee = txDesc.Fee + ancestorStats.Fees
+		prioItemMap[*tx.Hash()] = prioItem
+		hasParents := miningView.HasParents(tx.Hash())
+
+		if !hasParents || hasStats {
 			heap.Push(priorityQueue, prioItem)
+			prioritizedTxns[*tx.Hash()] = struct{}{}
+			blockUtxos.AddTxOuts(tx, nextBlockHeight, wire.NullBlockIndex,
+				isTreasuryEnabled)
+		}
+
+		if hasParents {
+			totalDescendantTxns++
 		}
 
 		// Merge the referenced outputs from the input transactions to
@@ -1392,7 +1444,7 @@ mempoolLoop:
 	}
 
 	log.Tracef("Priority queue len %d, dependers len %d",
-		priorityQueue.Len(), len(dependers))
+		priorityQueue.Len(), totalDescendantTxns)
 
 	// The starting block size is the size of the block header plus the max
 	// possible transaction count size, plus the size of the coinbase
@@ -1416,12 +1468,22 @@ mempoolLoop:
 		foundWinningTickets[ticketHash] = false
 	}
 
+	// Maintain lookup of transactions that have been included in the block
+	// template.
+	templateTxnMap := make(map[chainhash.Hash]struct{})
+
 	// Choose which transactions make it into the block.
+nextPriorityQueueItem:
 	for priorityQueue.Len() > 0 {
 		// Grab the highest priority (or highest fee per kilobyte
 		// depending on the sort order) transaction.
 		prioItem := heap.Pop(priorityQueue).(*txPrioItem)
-		tx := prioItem.tx
+		tx := prioItem.txDesc.Tx
+		delete(prioritizedTxns, *tx.Hash())
+
+		if _, exist := templateTxnMap[*tx.Hash()]; exist {
+			continue
+		}
 
 		// Store if this is an SStx or not.
 		isSStx := prioItem.txType == stake.TxTypeSStx
@@ -1442,7 +1504,7 @@ mempoolLoop:
 		}
 
 		// Grab the list of transactions which depend on this one (if any).
-		deps := dependers[*tx.Hash()]
+		deps := miningView.Children(tx.Hash())
 
 		// Skip TSpend if this block is not on a TVI or outside of the
 		// Tspend window.
@@ -1526,9 +1588,40 @@ mempoolLoop:
 			}
 		}
 
+		if miningView.IsRejected(tx.Hash()) {
+			// If the transaction or any of its ancestors have been rejected,
+			// discard the transaction.
+			continue
+		}
+
+		ancestors := miningView.Ancestors(tx.Hash())
+		ancestorStats, _ := miningView.AncestorStats(tx.Hash())
+		oldFee := prioItem.feePerKB
+		prioItem.feePerKB = calcFeePerKb(prioItem.txDesc, ancestorStats)
+
+		feeDecreased := oldFee > prioItem.feePerKB
+		if feeDecreased && ancestorStats.NumAncestors == 0 {
+			// If the fee decreased due to ancestors being included in the
+			// template and the transaction has no parents, then enqueue it one
+			// more time with an accurate feePerKb.
+			heap.Push(priorityQueue, prioItemMap[*tx.Hash()])
+			prioritizedTxns[*tx.Hash()] = struct{}{}
+			continue
+		}
+
+		if feeDecreased {
+			// Skip the transaction if the total feePerKb decreased.
+			// This addresses a vulnerability that would allow a low-fee
+			// transaction to have an inflated and inaccurate feePerKb based on
+			// ancestors that have already been included in the block template.
+			// The transaction will be added back to the priority queue when all
+			// parent transactions are included in the template.
+			continue
+		}
+
 		// Enforce maximum block size.  Also check for overflow.
 		txSize := uint32(tx.MsgTx().SerializeSize())
-		blockPlusTxSize := blockSize + txSize
+		blockPlusTxSize := blockSize + txSize + uint32(ancestorStats.SizeBytes)
 		if blockPlusTxSize < blockSize ||
 			blockPlusTxSize >= g.policy.BlockMaxSize {
 			log.Tracef("Skipping tx %s (size %v) because it "+
@@ -1536,38 +1629,20 @@ mempoolLoop:
 				"size %v, cur num tx %v", tx.Hash(), txSize,
 				blockSize, len(blockTxns))
 			logSkippedDeps(tx, deps)
+			miningView.Reject(tx.Hash())
 			continue
 		}
 
 		// Enforce maximum signature operations per block.  Also check
 		// for overflow.
-		numSigOps := int64(blockchain.CountSigOps(tx, false, isSSGen,
-			isTreasuryEnabled))
-		if blockSigOps+numSigOps < blockSigOps ||
-			blockSigOps+numSigOps > blockchain.MaxSigOpsPerBlock {
+		numSigOps := int64(prioItem.txDesc.TotalSigOps)
+		numSigOpsBundle := numSigOps + int64(ancestorStats.TotalSigOps)
+		if blockSigOps+numSigOpsBundle < blockSigOps ||
+			blockSigOps+numSigOpsBundle > blockchain.MaxSigOpsPerBlock {
 			log.Tracef("Skipping tx %s because it would "+
 				"exceed the maximum sigops per block", tx.Hash())
 			logSkippedDeps(tx, deps)
-			continue
-		}
-
-		// This isn't very expensive, but we do this check a number of times.
-		// Consider caching this in the mempool in the future. - Decred
-		numP2SHSigOps, err := blockchain.CountP2SHSigOps(tx, false,
-			isSSGen, blockUtxos, isTreasuryEnabled)
-		if err != nil {
-			log.Tracef("Skipping tx %s due to error in "+
-				"CountP2SHSigOps: %v", tx.Hash(), err)
-			logSkippedDeps(tx, deps)
-			continue
-		}
-		numSigOps += int64(numP2SHSigOps)
-		if blockSigOps+numSigOps < blockSigOps ||
-			blockSigOps+numSigOps > blockchain.MaxSigOpsPerBlock {
-			log.Tracef("Skipping tx %s because it would "+
-				"exceed the maximum sigops per block (p2sh)",
-				tx.Hash())
-			logSkippedDeps(tx, deps)
+			miningView.Reject(tx.Hash())
 			continue
 		}
 
@@ -1603,6 +1678,7 @@ mempoolLoop:
 				g.policy.TxMinFreeFee, blockPlusTxSize,
 				g.policy.BlockMinSize)
 			logSkippedDeps(tx, deps)
+			miningView.Reject(tx.Hash())
 			continue
 		}
 
@@ -1631,72 +1707,98 @@ mempoolLoop:
 				prioItem.priority < MinHighPriority {
 
 				heap.Push(priorityQueue, prioItem)
+				prioritizedTxns[*tx.Hash()] = struct{}{}
 				continue
 			}
 		}
 
-		// Ensure the transaction inputs pass all of the necessary
-		// preconditions before allowing it to be added to the block.
-		// The fraud proof is not checked because it will be filled in
-		// by the miner.
-		_, err = blockchain.CheckTransactionInputs(g.subsidyCache, tx,
-			nextBlockHeight, blockUtxos, false, g.chainParams,
-			isTreasuryEnabled)
-		if err != nil {
-			log.Tracef("Skipping tx %s due to error in "+
-				"CheckTransactionInputs: %v", tx.Hash(), err)
-			logSkippedDeps(tx, deps)
-			continue
-		}
-		err = blockchain.ValidateTransactionScripts(tx, blockUtxos,
-			scriptFlags, g.sigCache)
-		if err != nil {
-			log.Tracef("Skipping tx %s due to error in "+
-				"ValidateTransactionScripts: %v", tx.Hash(), err)
-			logSkippedDeps(tx, deps)
-			continue
+		txBundle := append(ancestors, prioItem.txDesc)
+		for _, bundledTx := range txBundle {
+			// Ensure the transaction inputs pass all of the necessary
+			// preconditions before allowing it to be added to the block.
+			// The fraud proof is not checked because it will be filled in
+			// by the miner.
+			_, err = blockchain.CheckTransactionInputs(g.subsidyCache,
+				bundledTx.Tx, nextBlockHeight, blockUtxos, false, g.chainParams,
+				isTreasuryEnabled)
+			if err != nil {
+				log.Tracef("Skipping tx %s due to error in "+
+					"CheckTransactionInputs: %v", bundledTx.Tx.Hash(), err)
+				logSkippedDeps(bundledTx.Tx, deps)
+				miningView.Reject(bundledTx.Tx.Hash())
+				continue nextPriorityQueueItem
+			}
+			err = blockchain.ValidateTransactionScripts(bundledTx.Tx,
+				blockUtxos, scriptFlags, g.sigCache)
+			if err != nil {
+				log.Tracef("Skipping tx %s due to error in "+
+					"ValidateTransactionScripts: %v", bundledTx.Tx.Hash(), err)
+				logSkippedDeps(bundledTx.Tx, deps)
+				miningView.Reject(bundledTx.Tx.Hash())
+				continue nextPriorityQueueItem
+			}
 		}
 
-		// Spend the transaction inputs in the block utxo view and add
-		// an entry for it to ensure any transactions which reference
-		// this one have it available as an input and can ensure they
-		// aren't double spending.
-		spendTransaction(blockUtxos, tx, nextBlockHeight, isTreasuryEnabled)
+		for _, bundledTxDesc := range txBundle {
+			bundledTx := bundledTxDesc.Tx
+			bundledTxHash := bundledTx.Hash()
 
-		// Add the transaction to the block, increment counters, and
-		// save the fees and signature operation counts to the block
-		// template.
-		blockTxns = append(blockTxns, tx)
-		blockSize += txSize
-		blockSigOps += numSigOps
-
-		// Accumulate the SStxs in the block, because only a certain number
-		// are allowed.
-		if isSStx {
-			numSStx++
-		}
-		if isSSGen {
-			foundWinningTickets[tx.MsgTx().TxIn[1].PreviousOutPoint.Hash] = true
-		}
-		if isTAdd {
-			numTAdds++
-		}
+			// Spend the transaction inputs in the block utxo view and add
+			// an entry for it to ensure any transactions which reference
+			// this one have it available as an input and can ensure they
+			// aren't double spending.
+			spendTransaction(blockUtxos, bundledTxDesc.Tx, nextBlockHeight,
+				isTreasuryEnabled)
 
-		txFeesMap[*tx.Hash()] = prioItem.fee
-		txSigOpCountsMap[*tx.Hash()] = numSigOps
+			// Add the transaction to the block, increment counters, and
+			// save the fees and signature operation counts to the block
+			// template.
+			blockTxns = append(blockTxns, bundledTx)
+			blockSize += uint32(bundledTx.MsgTx().SerializeSize())
+			bundledTxSigOps := int64(bundledTxDesc.TotalSigOps)
+			blockSigOps += bundledTxSigOps
+
+			// Accumulate the SStxs in the block, because only a certain number
+			// are allowed.
+			if bundledTxDesc.Type == stake.TxTypeSStx {
+				numSStx++
+			}
+			if bundledTxDesc.Type == stake.TxTypeSSGen {
+				foundWinningTickets[bundledTx.MsgTx().TxIn[1].PreviousOutPoint.Hash] = true
+			}
+			if isTreasuryEnabled && bundledTxDesc.Type == stake.TxTypeTAdd {
+				numTAdds++
+			}
 
-		log.Tracef("Adding tx %s (priority %.2f, feePerKB %.2f)",
-			prioItem.tx.Hash(), prioItem.priority, prioItem.feePerKB)
+			templateTxnMap[*bundledTxHash] = struct{}{}
+			txFeesMap[*bundledTxHash] = bundledTxDesc.Fee
+			txSigOpCountsMap[*bundledTxHash] = bundledTxSigOps
+
+			bundledPrioItem := prioItemMap[*bundledTxHash]
+			log.Tracef("Adding tx %s (priority %.2f, feePerKB %.2f)",
+				bundledTxHash, bundledPrioItem.priority,
+				bundledPrioItem.feePerKB)
+
+			// Remove transaction from mining view since it's been added to the
+			// block template.
+			bundledTxDeps := miningView.Children(bundledTxHash)
+			miningView.Remove(bundledTxHash, false)
+
+			// Add transactions which depend on this one (and also do not
+			// have any other unsatisfied dependencies) to the priority
+			// queue.
+			for _, childTx := range bundledTxDeps {
+				childTxHash := childTx.Tx.Hash()
+				if _, exist := prioritizedTxns[*childTxHash]; exist {
+					continue
+				}
 
-		// Add transactions which depend on this one (and also do not
-		// have any other unsatisfied dependencies) to the priority
-		// queue.
-		for _, item := range deps {
-			// Add the transaction to the priority queue if there
-			// are no more dependencies after this one.
-			delete(item.dependsOn, *tx.Hash())
-			if len(item.dependsOn) == 0 {
-				heap.Push(priorityQueue, item)
+				// Add the transaction to the priority queue if there
+				// are no more dependencies after this one.
+				if !miningView.HasParents(childTxHash) {
+					heap.Push(priorityQueue, prioItemMap[*childTxHash])
+					prioritizedTxns[*childTxHash] = struct{}{}
+				}
 			}
 		}
 	}
diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go
index 6d2b4806..a42f5c5a 100644
--- a/internal/rpcserver/rpcserver.go
+++ b/internal/rpcserver/rpcserver.go
@@ -4536,7 +4536,7 @@ func handleSendRawTransaction(_ context.Context, s *Server, cmd interface{}) (in
 			err = fmt.Errorf("rejected transaction %v: %v", tx.Hash(),
 				err)
 			log.Debugf("%v", err)
-			if mempool.IsErrorCode(rErr, mempool.ErrDuplicate) {
+			if errors.Is(rErr, mempool.ErrDuplicate) {
 				// This is an actual exact duplicate tx, so
 				// return the specific duplicate tx error.
 				return nil, rpcDuplicateTxError("%v", err)
diff --git a/server.go b/server.go
index facfda63..7532ba91 100644
--- a/server.go
+++ b/server.go
@@ -3175,15 +3175,16 @@ func newServer(ctx context.Context, listenAddrs []string, db database.DB, chainP
 
 	txC := mempool.Config{
 		Policy: mempool.Policy{
-			MaxTxVersion:         wire.TxVersionTreasury,
-			DisableRelayPriority: cfg.NoRelayPriority,
-			AcceptNonStd:         cfg.AcceptNonStd,
-			FreeTxRelayLimit:     cfg.FreeTxRelayLimit,
-			MaxOrphanTxs:         cfg.MaxOrphanTxs,
-			MaxOrphanTxSize:      mempool.MaxStandardTxSize,
-			MaxSigOpsPerTx:       blockchain.MaxSigOpsPerBlock / 5,
-			MinRelayTxFee:        cfg.minRelayTxFee,
-			AllowOldVotes:        cfg.AllowOldVotes,
+			EnableAncestorTracking: len(cfg.miningAddrs) > 0,
+			MaxTxVersion:           wire.TxVersionTreasury,
+			DisableRelayPriority:   cfg.NoRelayPriority,
+			AcceptNonStd:           cfg.AcceptNonStd,
+			FreeTxRelayLimit:       cfg.FreeTxRelayLimit,
+			MaxOrphanTxs:           cfg.MaxOrphanTxs,
+			MaxOrphanTxSize:        mempool.MaxStandardTxSize,
+			MaxSigOpsPerTx:         blockchain.MaxSigOpsPerBlock / 5,
+			MinRelayTxFee:          cfg.minRelayTxFee,
+			AllowOldVotes:          cfg.AllowOldVotes,
 			MaxVoteAge: func() uint16 {
 				switch chainParams.Net {
 				case wire.MainNet, wire.SimNet, wire.RegNet:


Test patch:
diff --git a/blockchain/chain_test.go b/blockchain/chain_test.go
index 3ac5300c..663f1105 100644
--- a/blockchain/chain_test.go
+++ b/blockchain/chain_test.go
@@ -157,8 +157,8 @@ func TestForceHeadReorg(t *testing.T) {
 	//
 	// rejectForceTipReorg forces the chain instance to reorganize the
 	// current tip of the main chain from the given block to the given
-	// block and expected it to be rejected with the provided error code.
-	rejectForceTipReorg := func(fromTipName, toTipName string, code ErrorCode) {
+	// block and expected it to be rejected with the provided error kind.
+	rejectForceTipReorg := func(fromTipName, toTipName string, kind ErrorKind) {
 		from := g.BlockByName(fromTipName)
 		to := g.BlockByName(toTipName)
 		t.Logf("Testing forced reorg from %s (hash %s, height %d) "+
@@ -175,25 +175,15 @@ func TestForceHeadReorg(t *testing.T) {
 				to.Header.Height)
 		}
 
-		// Ensure the error code is of the expected type and the reject
-		// code matches the value specified in the test instance.
-		var rerr RuleError
-		if !errors.As(err, &rerr) {
-			t.Fatalf("forced header reorg from block %q (hash %s, "+
-				"height %d) to block %q (hash %s, height %d) "+
-				"returned unexpected error type -- got %T, "+
-				"want blockchain.RuleError", fromTipName,
-				from.BlockHash(), from.Header.Height, toTipName,
-				to.BlockHash(), to.Header.Height, err)
-		}
-		if rerr.ErrorCode != code {
+		// Ensure the error kind is of the expected type and matches
+		// the value specified in the test instance.
+		if !errors.Is(err, kind) {
 			t.Fatalf("forced header reorg from block %q (hash %s, "+
 				"height %d) to block %q (hash %s, height %d) "+
 				"does not have expected reject code -- got %v, "+
 				"want %v", fromTipName,
 				from.BlockHash(), from.Header.Height, toTipName,
-				to.BlockHash(), to.Header.Height, rerr.ErrorCode,
-				code)
+				to.BlockHash(), to.Header.Height, err, kind)
 		}
 	}
 
diff --git a/blockchain/common_test.go b/blockchain/common_test.go
index e5fbc8a2..67843b78 100644
--- a/blockchain/common_test.go
+++ b/blockchain/common_test.go
@@ -532,8 +532,8 @@ func (g *chaingenHarness) AcceptTipBlock() {
 }
 
 // RejectBlock expects the block associated with the given name in the harness
-// generator to be rejected with the provided error code.
-func (g *chaingenHarness) RejectBlock(blockName string, code ErrorCode) {
+// generator to be rejected with the provided error kind.
+func (g *chaingenHarness) RejectBlock(blockName string, kind ErrorKind) {
 	g.t.Helper()
 
 	msgBlock := g.BlockByName(blockName)
@@ -548,27 +548,21 @@ func (g *chaingenHarness) RejectBlock(blockName string, code ErrorCode) {
 			blockName, block.Hash(), blockHeight)
 	}
 
-	// Ensure the error code is of the expected type and the reject code matches
+	// Ensure the error kind is of the expected type and matches
 	// the value specified in the test instance.
-	var rerr RuleError
-	if !errors.As(err, &rerr) {
-		g.t.Fatalf("block %q (hash %s, height %d) returned unexpected error "+
-			"type -- got %T, want blockchain.RuleError", blockName,
-			block.Hash(), blockHeight, err)
-	}
-	if rerr.ErrorCode != code {
+	if !errors.Is(err, kind) {
 		g.t.Fatalf("block %q (hash %s, height %d) does not have expected reject "+
 			"code -- got %v, want %v", blockName, block.Hash(), blockHeight,
-			rerr.ErrorCode, code)
+			err, kind)
 	}
 }
 
 // RejectTipBlock expects the current tip block associated with the harness
 // generator to be rejected with the provided error code.
-func (g *chaingenHarness) RejectTipBlock(code ErrorCode) {
+func (g *chaingenHarness) RejectTipBlock(kind ErrorKind) {
 	g.t.Helper()
 
-	g.RejectBlock(g.TipName(), code)
+	g.RejectBlock(g.TipName(), kind)
 }
 
 // ExpectTip expects the provided block to be the current tip of the main chain
diff --git a/blockchain/error_test.go b/blockchain/error_test.go
index c9b6993e..8100da2a 100644
--- a/blockchain/error_test.go
+++ b/blockchain/error_test.go
@@ -1,18 +1,20 @@
 // Copyright (c) 2014 The btcsuite developers
-// Copyright (c) 2015-2018 The Decred developers
+// Copyright (c) 2015-2020 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 (
+	"errors"
+	"io"
 	"testing"
 )
 
-// TestErrorCodeStringer tests the stringized output for the ErrorCode type.
-func TestErrorCodeStringer(t *testing.T) {
+// TestErrorKindStringer tests the stringized output for the ErrorKind type.
+func TestErrorKindStringer(t *testing.T) {
 	tests := []struct {
-		in   ErrorCode
+		in   ErrorKind
 		want string
 	}{
 		{ErrDuplicateBlock, "ErrDuplicateBlock"},
@@ -145,20 +147,13 @@ func TestErrorCodeStringer(t *testing.T) {
 		{ErrInvalidTAddChange, "ErrInvalidTAddChange"},
 		{ErrTooManyTAdds, "ErrTooManyTAdds"},
 		{ErrTicketExhaustion, "ErrTicketExhaustion"},
-		{0xffff, "Unknown ErrorCode (65535)"},
-	}
-
-	// Detect additional error codes that don't have the stringer added.
-	if len(tests)-1 != int(numErrorCodes) {
-		t.Errorf("It appears an error code was added without adding an " +
-			"associated stringer test")
 	}
 
 	t.Logf("Running %d tests", len(tests))
 	for i, test := range tests {
-		result := test.in.String()
+		result := test.in.Error()
 		if result != test.want {
-			t.Errorf("String #%d\n got: %s want: %s", i, result,
+			t.Errorf("#%d: got: %s want: %s", i, result,
 				test.want)
 			continue
 		}
@@ -185,51 +180,91 @@ func TestRuleError(t *testing.T) {
 	for i, test := range tests {
 		result := test.in.Error()
 		if result != test.want {
-			t.Errorf("Error #%d\n got: %s want: %s", i, result,
+			t.Errorf("Error #%d: got: %s want: %s", i, result,
 				test.want)
 			continue
 		}
 	}
 }
 
-// TestIsErrorCode ensures IsErrorCode works as intended.
-func TestIsErrorCode(t *testing.T) {
+// TestErrorKindIsAs ensures both ErrorKind and Error can be identified as being
+// a specific error kind via errors.Is and unwrapped via errors.As.
+func TestErrorKindIsAs(t *testing.T) {
 	tests := []struct {
-		name string
-		err  error
-		code ErrorCode
-		want bool
+		name      string
+		err       error
+		target    error
+		wantMatch bool
+		wantAs    ErrorKind
 	}{{
-		name: "ErrUnexpectedDifficulty testing for ErrUnexpectedDifficulty",
-		err:  ruleError(ErrUnexpectedDifficulty, ""),
-		code: ErrUnexpectedDifficulty,
-		want: true,
+		name:      "ErrDuplicateBlock == ErrDuplicateBlock",
+		err:       ErrDuplicateBlock,
+		target:    ErrDuplicateBlock,
+		wantMatch: true,
+		wantAs:    ErrDuplicateBlock,
+	}, {
+		name:      "RuleError.ErrDuplicateBlock == ErrDuplicateBlock",
+		err:       ruleError(ErrDuplicateBlock, ""),
+		target:    ErrDuplicateBlock,
+		wantMatch: true,
+		wantAs:    ErrDuplicateBlock,
 	}, {
-		name: "ErrHighHash testing for ErrHighHash",
-		err:  ruleError(ErrHighHash, ""),
-		code: ErrHighHash,
-		want: true,
+		name:      "RuleError.ErrDuplicateBlock == RuleError.ErrDuplicateBlock",
+		err:       ruleError(ErrDuplicateBlock, ""),
+		target:    ruleError(ErrDuplicateBlock, ""),
+		wantMatch: true,
+		wantAs:    ErrDuplicateBlock,
 	}, {
-		name: "ErrHighHash error testing for ErrUnexpectedDifficulty",
-		err:  ruleError(ErrHighHash, ""),
-		code: ErrUnexpectedDifficulty,
-		want: false,
+		name:      "ErrDuplicateBlock != ErrMissingParent",
+		err:       ErrDuplicateBlock,
+		target:    ErrMissingParent,
+		wantMatch: false,
+		wantAs:    ErrDuplicateBlock,
 	}, {
-		name: "ErrHighHash error testing for unknown error code",
-		err:  ruleError(ErrHighHash, ""),
-		code: 0xffff,
-		want: false,
+		name:      "RuleError.ErrDuplicateBlock != ErrMissingParent",
+		err:       ruleError(ErrDuplicateBlock, ""),
+		target:    ErrMissingParent,
+		wantMatch: false,
+		wantAs:    ErrDuplicateBlock,
 	}, {
-		name: "nil error testing for ErrUnexpectedDifficulty",
-		err:  nil,
-		code: ErrUnexpectedDifficulty,
-		want: false,
+		name:      "ErrDuplicateBlock != RuleError.ErrMissingParent",
+		err:       ErrDuplicateBlock,
+		target:    ruleError(ErrMissingParent, ""),
+		wantMatch: false,
+		wantAs:    ErrDuplicateBlock,
+	}, {
+		name:      "RuleError.ErrDuplicateBlock != RuleError.ErrMissingParent",
+		err:       ruleError(ErrDuplicateBlock, ""),
+		target:    ruleError(ErrMissingParent, ""),
+		wantMatch: false,
+		wantAs:    ErrDuplicateBlock,
+	}, {
+		name:      "RuleError.ErrDuplicateBlock != io.EOF",
+		err:       ruleError(ErrDuplicateBlock, ""),
+		target:    io.EOF,
+		wantMatch: false,
+		wantAs:    ErrDuplicateBlock,
 	}}
+
 	for _, test := range tests {
-		result := IsErrorCode(test.err, test.code)
-		if result != test.want {
-			t.Errorf("%s: unexpected result -- got: %v want: %v", test.name,
-				result, test.want)
+		// Ensure the error matches or not depending on the expected result.
+		result := errors.Is(test.err, test.target)
+		if result != test.wantMatch {
+			t.Errorf("%s: incorrect error identification -- got %v, want %v",
+				test.name, result, test.wantMatch)
+			continue
+		}
+
+		// Ensure the underlying error kind can be unwrapped is and is the
+		// expected kind.
+		var kind ErrorKind
+		if !errors.As(test.err, &kind) {
+			t.Errorf("%s: unable to unwrap to error kind", test.name)
+			continue
+		}
+		if kind != test.wantAs {
+			t.Errorf("%s: unexpected unwrapped error kind -- got %v, want %v",
+				test.name, kind, test.wantAs)
 			continue
 		}
 	}
diff --git a/blockchain/fullblocks_test.go b/blockchain/fullblocks_test.go
index 5a422a13..18890971 100644
--- a/blockchain/fullblocks_test.go
+++ b/blockchain/fullblocks_test.go
@@ -149,7 +149,7 @@ func TestFullBlocks(t *testing.T) {
 
 		var isOrphan bool
 		forkLen, err := chain.ProcessBlock(block, blockchain.BFNone)
-		if blockchain.IsErrorCode(err, blockchain.ErrMissingParent) {
+		if errors.Is(err, blockchain.ErrMissingParent) {
 			isOrphan = true
 			err = nil
 		}
@@ -194,18 +194,10 @@ func TestFullBlocks(t *testing.T) {
 
 		// Ensure the error code is of the expected type and the reject
 		// code matches the value specified in the test instance.
-		var rerr blockchain.RuleError
-		if !errors.As(err, &rerr) {
-			t.Fatalf("block %q (hash %s, height %d) returned "+
-				"unexpected error type -- got %T, want "+
-				"blockchain.RuleError", item.Name, block.Hash(),
-				blockHeight, err)
-		}
-		if rerr.ErrorCode != item.RejectCode {
+		if !errors.Is(err, item.RejectKind) {
 			t.Fatalf("block %q (hash %s, height %d) does not have "+
 				"expected reject code -- got %v, want %v",
-				item.Name, block.Hash(), blockHeight,
-				rerr.ErrorCode, item.RejectCode)
+				item.Name, block.Hash(), blockHeight, err, item.RejectKind)
 		}
 	}
 
diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go
index 1f50cfb6..88ad3819 100644
--- a/blockchain/fullblocktests/generate.go
+++ b/blockchain/fullblocktests/generate.go
@@ -93,7 +93,7 @@ func (b AcceptedBlock) FullBlockTestInstance() {}
 type RejectedBlock struct {
 	Name       string
 	Block      *wire.MsgBlock
-	RejectCode blockchain.ErrorCode
+	RejectKind blockchain.ErrorKind
 }
 
 // Ensure RejectedBlock implements the TestInstance interface.
@@ -402,8 +402,8 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
 	acceptBlock := func(blockName string, block *wire.MsgBlock, isMainChain, isOrphan bool) TestInstance {
 		return AcceptedBlock{blockName, block, isMainChain, isOrphan}
 	}
-	rejectBlock := func(blockName string, block *wire.MsgBlock, code blockchain.ErrorCode) TestInstance {
-		return RejectedBlock{blockName, block, code}
+	rejectBlock := func(blockName string, block *wire.MsgBlock, kind blockchain.ErrorKind) TestInstance {
+		return RejectedBlock{blockName, block, kind}
 	}
 	rejectNonCanonicalBlock := func(blockName string, block *wire.MsgBlock) TestInstance {
 		blockHeight := int32(block.Header.Height)
@@ -449,9 +449,9 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
 			expectTipBlock(tipName, g.BlockByName(tipName)),
 		})
 	}
-	rejected := func(code blockchain.ErrorCode) {
+	rejected := func(kind blockchain.ErrorKind) {
 		tests = append(tests, []TestInstance{
-			rejectBlock(g.TipName(), g.Tip(), code),
+			rejectBlock(g.TipName(), g.Tip(), kind),
 		})
 	}
 	rejectedNonCanonical := func() {
diff --git a/blockchain/stake/error_test.go b/blockchain/stake/error_test.go
index cd8017d3..ec3a1afc 100644
--- a/blockchain/stake/error_test.go
+++ b/blockchain/stake/error_test.go
@@ -1,5 +1,5 @@
 // Copyright (c) 2014 Conformal Systems LLC.
-// Copyright (c) 2015-2016 The Decred developers
+// Copyright (c) 2015-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/blockchain/stake/internal/ticketdb/error_test.go b/blockchain/stake/internal/ticketdb/error_test.go
index 874ad3e0..0d4e4e0e 100644
--- a/blockchain/stake/internal/ticketdb/error_test.go
+++ b/blockchain/stake/internal/ticketdb/error_test.go
@@ -1,5 +1,5 @@
 // Copyright (c) 2014 Conformal Systems LLC.
-// Copyright (c) 2015-2016 The Decred developers
+// Copyright (c) 2015-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/blockchain/stake/internal/tickettreap/benchmark_test.go b/blockchain/stake/internal/tickettreap/benchmark_test.go
index 67a4d249..9a927d9a 100644
--- a/blockchain/stake/internal/tickettreap/benchmark_test.go
+++ b/blockchain/stake/internal/tickettreap/benchmark_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2016-2017 The Decred developers
+// Copyright (c) 2016-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/blockchain/stake/lottery_test.go b/blockchain/stake/lottery_test.go
index bf28d219..d2edade2 100644
--- a/blockchain/stake/lottery_test.go
+++ b/blockchain/stake/lottery_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2019 The Decred developers
+// Copyright (c) 2015-2020 The Decred developers
 // Use of this source code is governed by an ISC
 // license that can be found in the LICENSE file.
 
diff --git a/blockchain/validate_test.go b/blockchain/validate_test.go
index 90fb6f44..9bc26f8c 100644
--- a/blockchain/validate_test.go
+++ b/blockchain/validate_test.go
@@ -602,15 +602,10 @@ func TestTxValidationErrors(t *testing.T) {
 
 	// Ensure transaction is rejected due to being too large.
 	err := CheckTransactionSanity(tx, chaincfg.MainNetParams(), noTreasury)
-	var rerr RuleError
-	if !errors.As(err, &rerr) {
-		t.Fatalf("CheckTransactionSanity: unexpected error type for "+
-			"transaction that is too large -- got %T", err)
-	}
-	if rerr.ErrorCode != ErrTxTooBig {
+	if !errors.Is(err, ErrTxTooBig) {
 		t.Fatalf("CheckTransactionSanity: unexpected error code for "+
 			"transaction that is too large -- got %v, want %v",
-			rerr.ErrorCode, ErrTxTooBig)
+			err, ErrTxTooBig)
 	}
 }
 
@@ -667,7 +662,7 @@ func TestCheckConnectBlockTemplate(t *testing.T) {
 				block.Hash(), blockHeight, err)
 		}
 	}
-	rejectedBlockTemplate := func(code ErrorCode) {
+	rejectedBlockTemplate := func(kind ErrorKind) {
 		msgBlock := g.Tip()
 		blockHeight := msgBlock.Header.Height
 		block := dcrutil.NewBlock(msgBlock)
@@ -683,18 +678,10 @@ func TestCheckConnectBlockTemplate(t *testing.T) {
 
 		// Ensure the error code is of the expected type and the reject
 		// code matches the value specified in the test instance.
-		var rerr RuleError
-		if !errors.As(err, &rerr) {
-			t.Fatalf("block template %q (hash %s, height %d) "+
-				"returned unexpected error type -- got %T, want "+
-				"blockchain.RuleError", g.TipName(),
-				block.Hash(), blockHeight, err)
-		}
-		if rerr.ErrorCode != code {
+		if !errors.Is(err, kind) {
 			t.Fatalf("block template %q (hash %s, height %d) does "+
 				"not have expected reject code -- got %v, want %v",
-				g.TipName(), block.Hash(), blockHeight,
-				rerr.ErrorCode, code)
+				g.TipName(), block.Hash(), blockHeight, err, kind)
 		}
 	}
 
@@ -1054,16 +1041,6 @@ func TestCheckTicketExhaustion(t *testing.T) {
 	params.StakeEnabledHeight = stakeEnabledHeight
 	params.StakeValidationHeight = stakeValidationHeight
 
-	// isErr is a convenience func which acts as a limited version of errors.Is
-	// until the package is converted to support it at which point this can be
-	// removed.
-	isErr := func(err error, target error) bool {
-		if (err == nil) != (target == nil) {
-			return false
-		}
-		return target == nil || IsErrorCode(err, target.(RuleError).ErrorCode)
-	}
-
 	// ticketInfo is used to control the tests by specifying the details about
 	// how many fake blocks to create with the specified number of tickets.
 	type ticketInfo struct {
@@ -1085,7 +1062,7 @@ func TestCheckTicketExhaustion(t *testing.T) {
 			{126, 0}, // height: 126, 0 live, 0 immature
 		},
 		newBlockTix: 0, // extending height: 126, 0 live, 0 immature
-		err:         ruleError(ErrTicketExhaustion, ""),
+		err:         ErrTicketExhaustion,
 	}, {
 		// Reach inevitable ticket exhaustion by not including any ticket
 		// purchases up to just before the final possible block prior to svh
@@ -1096,7 +1073,7 @@ func TestCheckTicketExhaustion(t *testing.T) {
 			{126, 0}, // height: 126, 0 live, 0 immature
 		},
 		newBlockTix: 4, // extending height: 126, 0 live, 4 immature
-		err:         ruleError(ErrTicketExhaustion, ""),
+		err:         ErrTicketExhaustion,
 	}, {
 		// Construct chain such that there are no ticket purchases up to just
 		// before the final possible block prior to svh that can prevent ticket
@@ -1119,7 +1096,7 @@ func TestCheckTicketExhaustion(t *testing.T) {
 			{109, 0}, // height: 126, 4 live, 0 immature
 		},
 		newBlockTix: 0, // extending height: 126, 4 live, 0 immature
-		err:         ruleError(ErrTicketExhaustion, ""),
+		err:         ErrTicketExhaustion,
 	}, {
 		name: "just enough live tickets at 1st possible exhaustion",
 		ticketInfo: []ticketInfo{
@@ -1140,7 +1117,7 @@ func TestCheckTicketExhaustion(t *testing.T) {
 			{110, 0}, // height: 127, 5 live, 0 immature
 		},
 		newBlockTix: 0, // extending height: 127, 5 live, 0 immature
-		err:         ruleError(ErrTicketExhaustion, ""),
+		err:         ErrTicketExhaustion,
 	}, {
 		// Reach inevitable ticket exhaustion in the second possible block that
 		// it can happen with one live ticket less than needed to prevent it.
@@ -1151,7 +1128,7 @@ func TestCheckTicketExhaustion(t *testing.T) {
 			{110, 0}, // height: 127, 9 live, 0 immature
 		},
 		newBlockTix: 0, // extending height: 127, 9 live, 0 immature
-		err:         ruleError(ErrTicketExhaustion, ""),
+		err:         ErrTicketExhaustion,
 	}, {
 		// Construct chain to one block before svh such that there are exactly
 		// enough live tickets to prevent exhaustion.
@@ -1212,7 +1189,7 @@ func TestCheckTicketExhaustion(t *testing.T) {
 			{75, 0},  // height: 226, 85 live, 0 immature
 		},
 		newBlockTix: 0, // extending height: 226, 85 live, 0 immature
-		err:         ruleError(ErrTicketExhaustion, ""),
+		err:         ErrTicketExhaustion,
 	}}
 
 	for _, test := range tests {
@@ -1261,7 +1238,7 @@ func TestCheckTicketExhaustion(t *testing.T) {
 
 		// Ensure the expected result is returned from ticket exhaustion check.
 		err := bc.CheckTicketExhaustion(&node.hash, test.newBlockTix)
-		if !isErr(err, test.err) {
+		if !errors.Is(err, test.err) {
 			t.Errorf("%q: mismatched err -- got %v, want %v", test.name, err,
 				test.err)
 			continue
diff --git a/internal/mempool/error_test.go b/internal/mempool/error_test.go
new file mode 100644
index 00000000..c4282042
--- /dev/null
+++ b/internal/mempool/error_test.go
@@ -0,0 +1,160 @@
+// Copyright (c) 2020 The Decred developers
+// Use of this source code is governed by an ISC
+// license that can be found in the LICENSE file.
+
+package mempool
+
+import (
+	"errors"
+	"io"
+	"testing"
+)
+
+// TestErrorKindStringer tests the stringized output for the ErrorKind type.
+func TestErrorKindStringer(t *testing.T) {
+	tests := []struct {
+		in   ErrorKind
+		want string
+	}{
+		{ErrInvalid, "ErrInvalid"},
+		{ErrOrphanPolicyViolation, "ErrOrphanPolicyViolation"},
+		{ErrMempoolDoubleSpend, "ErrMempoolDoubleSpend"},
+		{ErrAlreadyVoted, "ErrorAlreadyVoted"},
+		{ErrDuplicate, "ErrDuplicate"},
+		{ErrCoinbase, "ErrCoinbase"},
+		{ErrExpired, "ErrExpired"},
+		{ErrNonStandard, "ErrNonStandard"},
+		{ErrDustOutput, "ErrDustOutput"},
+		{ErrInsufficientFee, "ErrInsufficientFee"},
+		{ErrTooManyVotes, "ErrTooManyVotes"},
+		{ErrDuplicateRevocation, "ErrDuplicateRevocation"},
+		{ErrOldVote, "ErrOldVote"},
+		{ErrAlreadyExists, "ErrAlreadyExists"},
+		{ErrSeqLockUnmet, "ErrSeqLockUnmet"},
+		{ErrInsufficientPriority, "ErrInsufficientPriority"},
+		{ErrFeeTooHigh, "ErrFeeTooHigh"},
+		{ErrOrphan, "ErrOrphan"},
+		{ErrTooManyTSpends, "ErrTooManyTSpends"},
+		{ErrTSpendMinedOnAncestor, "ErrTSpendMinedOnAncestor"},
+		{ErrTSpendInvalidExpiry, "ErrTSpendInvalidExpiry"},
+	}
+
+	t.Logf("Running %d tests", len(tests))
+	for i, test := range tests {
+		result := test.in.Error()
+		if result != test.want {
+			t.Errorf("#%d\n got: %s want: %s", i, result,
+				test.want)
+			continue
+		}
+	}
+}
+
+// TestRuleError tests the error output for the RuleError type.
+func TestRuleError(t *testing.T) {
+	tests := []struct {
+		in   RuleError
+		want string
+	}{
+		{
+			RuleError{Description: "duplicate block"},
+			"duplicate block",
+		},
+		{
+			RuleError{Description: "human-readable error"},
+			"human-readable error",
+		},
+	}
+
+	t.Logf("Running %d tests", len(tests))
+	for i, test := range tests {
+		result := test.in.Error()
+		if result != test.want {
+			t.Errorf("#%d\n got: %s want: %s", i, result, test.want)
+			continue
+		}
+	}
+}
+
+// TestErrorKindIsAs ensures both ErrorKind and Error can be identified as being
+// a specific error kind via errors.Is and unwrapped via errors.As.
+func TestErrorKindIsAs(t *testing.T) {
+	tests := []struct {
+		name      string
+		err       error
+		target    error
+		wantMatch bool
+		wantAs    ErrorKind
+	}{{
+		name:      "ErrOrphanPolicyViolation == ErrOrphanPolicyViolation",
+		err:       ErrOrphanPolicyViolation,
+		target:    ErrOrphanPolicyViolation,
+		wantMatch: true,
+		wantAs:    ErrOrphanPolicyViolation,
+	}, {
+		name:      "RuleError.ErrOrphanPolicyViolation == ErrOrphanPolicyViolation",
+		err:       txRuleError(ErrOrphanPolicyViolation, ""),
+		target:    ErrOrphanPolicyViolation,
+		wantMatch: true,
+		wantAs:    ErrOrphanPolicyViolation,
+	}, {
+		name:      "RuleError.ErrOrphanPolicyViolation == RuleError.ErrOrphanPolicyViolation",
+		err:       txRuleError(ErrOrphanPolicyViolation, ""),
+		target:    txRuleError(ErrOrphanPolicyViolation, ""),
+		wantMatch: true,
+		wantAs:    ErrOrphanPolicyViolation,
+	}, {
+		name:      "ErrOrphanPolicyViolation != ErrMempoolDoubleSpend",
+		err:       ErrOrphanPolicyViolation,
+		target:    ErrMempoolDoubleSpend,
+		wantMatch: false,
+		wantAs:    ErrOrphanPolicyViolation,
+	}, {
+		name:      "RuleError.ErrOrphanPolicyViolation != ErrMempoolDoubleSpend",
+		err:       txRuleError(ErrOrphanPolicyViolation, ""),
+		target:    ErrMempoolDoubleSpend,
+		wantMatch: false,
+		wantAs:    ErrOrphanPolicyViolation,
+	}, {
+		name:      "ErrOrphanPolicyViolation != RuleError.ErrMempoolDoubleSpend",
+		err:       ErrOrphanPolicyViolation,
+		target:    txRuleError(ErrMempoolDoubleSpend, ""),
+		wantMatch: false,
+		wantAs:    ErrOrphanPolicyViolation,
+	}, {
+		name:      "RuleError.ErrOrphanPolicyViolation != RuleError.ErrMempoolDoubleSpend",
+		err:       txRuleError(ErrOrphanPolicyViolation, ""),
+		target:    txRuleError(ErrMempoolDoubleSpend, ""),
+		wantMatch: false,
+		wantAs:    ErrOrphanPolicyViolation,
+	}, {
+		name:      "RuleError.ErrOrphanPolicyViolation != io.EOF",
+		err:       txRuleError(ErrOrphanPolicyViolation, ""),
+		target:    io.EOF,
+		wantMatch: false,
+		wantAs:    ErrOrphanPolicyViolation,
+	}}
+
+	for _, test := range tests {
+		// Ensure the error matches or not depending on the expected result.
+		result := errors.Is(test.err, test.target)
+		if result != test.wantMatch {
+			t.Errorf("%s: incorrect error identification -- got %v, want %v",
+				test.name, result, test.wantMatch)
+			continue
+		}
+
+		// Ensure the underlying error kind can be unwrapped is and is the
+		// expected kind.
+		var kind ErrorKind
+		if !errors.As(test.err, &kind) {
+			t.Errorf("%s: unable to unwrap to error kind", test.name)
+			continue
+		}
+		if kind != test.wantAs {
+			t.Errorf("%s: unexpected unwrapped error kind -- got %v, want %v",
+				test.name, kind, test.wantAs)
+			continue
+		}
+	}
+}
diff --git a/internal/mempool/mempool_test.go b/internal/mempool/mempool_test.go
index 3d0e83d3..438aa1f1 100644
--- a/internal/mempool/mempool_test.go
+++ b/internal/mempool/mempool_test.go
@@ -200,7 +200,7 @@ func (s *fakeChain) CalcSequenceLock(tx *dcrutil.Tx, view *blockchain.UtxoViewpo
 				"either does not exist or has already been spent",
 				txIn.PreviousOutPoint, tx.Hash(), txInIndex)
 			return nil, blockchain.RuleError{
-				ErrorCode:   blockchain.ErrMissingTxOut,
+				Err:         blockchain.ErrMissingTxOut,
 				Description: str,
 			}
 		}
@@ -783,13 +783,14 @@ func newPoolHarness(chainParams *chaincfg.Params) (*poolHarness, []spendableOutp
 		chain: chain,
 		txPool: New(&Config{
 			Policy: Policy{
-				MaxTxVersion:         wire.TxVersionTreasury,
-				DisableRelayPriority: true,
-				FreeTxRelayLimit:     15.0,
-				MaxOrphanTxs:         5,
-				MaxOrphanTxSize:      1000,
-				MaxSigOpsPerTx:       blockchain.MaxSigOpsPerBlock / 5,
-				MinRelayTxFee:        1000, // 1 Satoshi per byte
+				EnableAncestorTracking: true,
+				MaxTxVersion:           wire.TxVersionTreasury,
+				DisableRelayPriority:   true,
+				FreeTxRelayLimit:       15.0,
+				MaxOrphanTxs:           5,
+				MaxOrphanTxSize:        1000,
+				MaxSigOpsPerTx:         blockchain.MaxSigOpsPerBlock / 5,
+				MinRelayTxFee:          1000, // 1 Atom per byte
 				MaxVoteAge: func() uint16 {
 					switch chainParams.Net {
 					case wire.MainNet, wire.SimNet, wire.RegNet:
@@ -1065,7 +1066,7 @@ func TestVoteOrphan(t *testing.T) {
 
 	// Ensure the vote is rejected because it is an orphan.
 	_, err = harness.txPool.ProcessTransaction(vote, false, false, true, 0)
-	if !IsErrorCode(err, ErrOrphan) {
+	if !errors.Is(err, ErrOrphan) {
 		t.Fatalf("Process Transaction: did not get expected ErrOrphan")
 	}
 	testPoolMembership(tc, vote, false, false)
@@ -1136,7 +1137,7 @@ func TestRevocationOrphan(t *testing.T) {
 	// Ensure the vote is rejected because it is an orphan.
 	_, err = harness.txPool.ProcessTransaction(revocation, false, false, true,
 		0)
-	if !IsErrorCode(err, ErrOrphan) {
+	if !errors.Is(err, ErrOrphan) {
 		t.Fatalf("Process Transaction: did not get expected " +
 			"ErrTooManyVotes error code")
 	}
@@ -1201,7 +1202,7 @@ func TestOrphanReject(t *testing.T) {
 			t.Fatalf("ProcessTransaction: did not fail on orphan "+
 				"%v when allow orphans flag is false", tx.Hash())
 		}
-		if !IsErrorCode(err, ErrOrphan) {
+		if !errors.Is(err, ErrOrphan) {
 			t.Fatalf("ProcessTransaction: unexpected error -- got %v, want %v",
 				err, ErrOrphan)
 		}
@@ -1860,7 +1861,7 @@ func TestSequenceLockAcceptance(t *testing.T) {
 			case acceptSeqLocks && !test.valid && err == nil:
 				t.Fatalf("%s: did not reject tx", test.name)
 
-			case acceptSeqLocks && !test.valid && !IsErrorCode(err, ErrSeqLockUnmet):
+			case acceptSeqLocks && !test.valid && !errors.Is(err, ErrSeqLockUnmet):
 				t.Fatalf("%s: did not get expected ErrSeqLockUnmet",
 					test.name)
 			}
@@ -1977,7 +1978,7 @@ func TestMaxVoteDoubleSpendRejection(t *testing.T) {
 			t.Fatalf("ProcessTransaction: accepted double-spending vote with " +
 				"more than max allowed")
 		}
-		if !IsErrorCode(err, ErrTooManyVotes) {
+		if !errors.Is(err, ErrTooManyVotes) {
 			t.Fatalf("Process Transaction: did not get expected " +
 				"ErrTooManyVotes error code")
 		}
@@ -2015,7 +2016,7 @@ func TestMaxVoteDoubleSpendRejection(t *testing.T) {
 	// in the transaction pool, and not reported as available.
 	vote = votes[maxVoteDoubleSpends+1]
 	_, err = harness.txPool.ProcessTransaction(vote, false, false, true, 0)
-	if !IsErrorCode(err, ErrTooManyVotes) {
+	if !errors.Is(err, ErrTooManyVotes) {
 		t.Fatalf("Process Transaction: did not get expected " +
 			"ErrTooManyVotes error code")
 	}
@@ -2088,7 +2089,7 @@ func TestDuplicateVoteRejection(t *testing.T) {
 	// ensure it is not in the orphan pool, not in the transaction pool, and not
 	// reported as available.
 	_, err = harness.txPool.ProcessTransaction(dupVote, false, false, true, 0)
-	if !IsErrorCode(err, ErrAlreadyVoted) {
+	if !errors.Is(err, ErrAlreadyVoted) {
 		t.Fatalf("Process Transaction: did not get expected " +
 			"ErrTooManyVotes error code")
 	}
@@ -2138,7 +2139,7 @@ func TestDuplicateTxError(t *testing.T) {
 	// Ensure a second attempt to process the tx is rejected with the
 	// correct error code and that the transaction remains in the pool.
 	_, err = harness.txPool.ProcessTransaction(tx, true, false, true, 0)
-	if !IsErrorCode(err, ErrDuplicate) {
+	if !errors.Is(err, ErrDuplicate) {
 		t.Fatalf("ProcessTransaction: did get the expected ErrDuplicate")
 	}
 	testPoolMembership(tc, tx, false, true)
@@ -2162,7 +2163,7 @@ func TestDuplicateTxError(t *testing.T) {
 
 	// The second call should fail with the expected ErrDuplicate error.
 	_, err = harness.txPool.ProcessTransaction(orphan, true, false, true, 0)
-	if !IsErrorCode(err, ErrDuplicate) {
+	if !errors.Is(err, ErrDuplicate) {
 		t.Fatalf("ProcessTransaction: did not get expected ErrDuplicate")
 	}
 	testPoolMembership(tc, orphan, true, false)
@@ -2207,7 +2208,7 @@ func TestMempoolDoubleSpend(t *testing.T) {
 	// pool and the double spend is not added to the pool.
 	_, err = harness.txPool.ProcessTransaction(doubleSpendTx, true, false, true,
 		0)
-	if !IsErrorCode(err, ErrMempoolDoubleSpend) {
+	if !errors.Is(err, ErrMempoolDoubleSpend) {
 		t.Fatalf("ProcessTransaction: did not get expected ErrMempoolDoubleSpend")
 	}
 	testPoolMembership(tc, tx, false, true)
@@ -2451,13 +2452,13 @@ func TestHandlesTSpends(t *testing.T) {
 
 	// Helper that attempts to add and asserts the given tspend is rejected
 	// with the given error code.
-	rejectTSpend := func(tx *dcrutil.Tx, errCode ErrorCode) {
+	rejectTSpend := func(tx *dcrutil.Tx, kind ErrorKind) {
 		t.Helper()
 		_, err = harness.txPool.ProcessTransaction(tx, true,
 			false, true, 0)
-		if !IsErrorCode(err, errCode) {
+		if !errors.Is(err, kind) {
 			t.Fatalf("Unexpected error while processing rejected tspend. "+
-				"want=%v got=%#v", errCode, err)
+				"want=%v got=%#v", kind, err)
 		}
 		testPoolMembership(tc, tx, false, false)
 	}
@@ -2467,17 +2468,13 @@ func TestHandlesTSpends(t *testing.T) {
 	//
 	// TODO: unify with the above after mempool errors correctly handle
 	// errors.Is/As.
-	rejectTSpendChainError := func(tx *dcrutil.Tx, errCode blockchain.ErrorCode) {
+	rejectTSpendChainError := func(tx *dcrutil.Tx, kind blockchain.ErrorKind) {
 		t.Helper()
 		_, err = harness.txPool.ProcessTransaction(tx, true,
 			false, true, 0)
-		rerr, ok := err.(RuleError)
-		if !ok {
-			t.Fatalf("Returned error is not a rule error: %#v", err)
-		}
-		if !blockchain.IsErrorCode(rerr.Err, errCode) {
+		if !errors.Is(err, kind) {
 			t.Fatalf("Unexpected error while processing rejected tspend. "+
-				"want=%v got=%#v", errCode, err)
+				"want=%v got=%#v", kind, err)
 		}
 		testPoolMembership(tc, tx, false, false)
 	}
@@ -2750,3 +2747,445 @@ func TestHandlesTAdds(t *testing.T) {
 	}
 	acceptTAdd(tadd)
 }
+
+func (p *poolHarness) countTotalSigOps(tx *dcrutil.Tx, txType stake.TxType) (int, error) {
+	isVote := txType == stake.TxTypeSSGen
+	isStakeBase := txType == stake.TxTypeSSGen
+	utxoView, err := p.txPool.fetchInputUtxos(tx, p.treasuryActive)
+	if err != nil {
+		return 0, err
+	}
+
+	sigOps := blockchain.CountSigOps(tx, false, isVote, p.treasuryActive)
+	p2shSigOps, err := blockchain.CountP2SHSigOps(tx, false, isStakeBase,
+		utxoView, p.treasuryActive)
+	if err != nil {
+		return 0, err
+	}
+
+	return sigOps + p2shSigOps, nil
+}
+
+// descendants returns a collection of transactions in the graph that depend on
+// the provided transaction hash.
+func (g *txDescGraph) descendants(txHash *chainhash.Hash) []*chainhash.Hash {
+	seen := map[chainhash.Hash]struct{}{}
+	descendants := make([]*chainhash.Hash, 0)
+	g.forEachDescendant(txHash, seen, func(descendant *mining.TxDesc) {
+		descendants = append(descendants, descendant.Tx.Hash())
+	})
+	return descendants
+}
+
+// Tests the behavior of the mining view when returned from a mempool with
+// containing a transaction chain depicted as:
+//
+//                       /--> d --> f
+//                       |
+//                 |--> b --|
+// <coinbase>  --> a        |-->e
+//                 |--> c --|
+//
+//
+func TestMiningView(t *testing.T) {
+	harness, spendableOuts, err := newPoolHarness(chaincfg.MainNetParams())
+	if err != nil {
+		t.Fatalf("unable to create test pool: %v", err)
+	}
+
+	applyTxFee := func(fee int64) func(*wire.MsgTx) {
+		return func(tx *wire.MsgTx) {
+			tx.TxOut[0].Value -= fee
+		}
+	}
+
+	// All transactions have the same number of outputs
+	// to keep the size uniform.
+	txA, _ := harness.CreateSignedTx([]spendableOutput{
+		spendableOuts[0],
+	}, 2, applyTxFee(1000))
+
+	txB, _ := harness.CreateSignedTx([]spendableOutput{
+		txOutToSpendableOut(txA, 0, wire.TxTreeRegular),
+	}, 2, applyTxFee(5000))
+
+	txC, _ := harness.CreateSignedTx([]spendableOutput{
+		txOutToSpendableOut(txA, 1, wire.TxTreeRegular),
+	}, 2, applyTxFee(5000))
+
+	txD, _ := harness.CreateSignedTx([]spendableOutput{
+		txOutToSpendableOut(txB, 0, wire.TxTreeRegular),
+	}, 2, applyTxFee(5000))
+
+	txE, _ := harness.CreateSignedTx([]spendableOutput{
+		txOutToSpendableOut(txB, 1, wire.TxTreeRegular),
+		txOutToSpendableOut(txC, 0, wire.TxTreeRegular),
+	}, 2, applyTxFee(5000))
+
+	txF, _ := harness.CreateSignedTx([]spendableOutput{
+		txOutToSpendableOut(txD, 0, wire.TxTreeRegular),
+	}, 2, applyTxFee(0))
+
+	// Add all to mempool.
+	allTxns := []*dcrutil.Tx{txB, txA, txC, txD, txE}
+	for index, tx := range allTxns {
+		_, err := harness.txPool.ProcessTransaction(tx,
+			true, false, true, 0)
+		if err != nil {
+			t.Fatalf("ProcessTransaction: failed to accept valid "+
+				"transaction at index %d: %v", index, err)
+		}
+	}
+
+	txALen := int64(txA.MsgTx().SerializeSize())
+	txBLen := int64(txB.MsgTx().SerializeSize())
+	txCLen := int64(txC.MsgTx().SerializeSize())
+
+	txATotalSigOps, err := harness.countTotalSigOps(txA, stake.TxTypeRegular)
+	if err != nil {
+		t.Fatalf("failed to count sigops for txA %v", err)
+	}
+
+	txBTotalSigOps, err := harness.countTotalSigOps(txB, stake.TxTypeRegular)
+	if err != nil {
+		t.Fatalf("failed to count sigops for txB %v", err)
+	}
+
+	txCTotalSigOps, err := harness.countTotalSigOps(txC, stake.TxTypeRegular)
+	if err != nil {
+		t.Fatalf("failed to count sigops for txC %v", err)
+	}
+
+	tests := []struct {
+		name                 string
+		miningView           mining.TxMiningView
+		subject              *dcrutil.Tx
+		expectedAncestorFees int64
+		expectedSizeBytes    int64
+		expectedSigOps       int
+		ancestors            []*dcrutil.Tx
+		descendants          map[chainhash.Hash]*dcrutil.Tx
+		orderedAncestors     [][]*dcrutil.Tx
+	}{{
+		name:                 "Tx A",
+		subject:              txA,
+		expectedAncestorFees: 0,
+		expectedSizeBytes:    0,
+		expectedSigOps:       0,
+		ancestors:            nil,
+		descendants: map[chainhash.Hash]*dcrutil.Tx{
+			txB.MsgTx().TxHash(): txB,
+			txC.MsgTx().TxHash(): txC,
+			txD.MsgTx().TxHash(): txD,
+			txE.MsgTx().TxHash(): txE,
+		},
+		orderedAncestors: nil,
+	}, {
+		name:                 "Tx B",
+		subject:              txB,
+		expectedAncestorFees: 1000,
+		expectedSizeBytes:    txALen,
+		expectedSigOps:       txATotalSigOps,
+		ancestors:            []*dcrutil.Tx{txA},
+		descendants: map[chainhash.Hash]*dcrutil.Tx{
+			txD.MsgTx().TxHash(): txD,
+			txE.MsgTx().TxHash(): txE,
+		},
+		orderedAncestors: [][]*dcrutil.Tx{
+			{txA},
+		},
+	}, {
+		name:                 "Tx C",
+		subject:              txC,
+		expectedAncestorFees: 1000,
+		expectedSizeBytes:    txALen,
+		expectedSigOps:       txATotalSigOps,
+		ancestors:            []*dcrutil.Tx{txA},
+		descendants: map[chainhash.Hash]*dcrutil.Tx{
+			txE.MsgTx().TxHash(): txE,
+		},
+		orderedAncestors: [][]*dcrutil.Tx{
+			{txA},
+		},
+	}, {
+		name:                 "Tx D",
+		subject:              txD,
+		expectedAncestorFees: 6000,
+		expectedSizeBytes:    txALen + txBLen,
+		expectedSigOps:       txATotalSigOps + txBTotalSigOps,
+		ancestors:            []*dcrutil.Tx{txA, txB},
+		descendants:          map[chainhash.Hash]*dcrutil.Tx{},
+		orderedAncestors: [][]*dcrutil.Tx{
+			{txA, txB},
+		},
+	}, {
+		name:                 "Tx E",
+		subject:              txE,
+		expectedAncestorFees: 11000,
+		expectedSizeBytes:    txALen + txBLen + txCLen,
+		expectedSigOps: txATotalSigOps + txBTotalSigOps +
+			txCTotalSigOps,
+		ancestors:   []*dcrutil.Tx{txA, txB, txC},
+		descendants: map[chainhash.Hash]*dcrutil.Tx{},
+		orderedAncestors: [][]*dcrutil.Tx{
+			{txA, txB, txC},
+			{txA, txC, txB},
+		},
+	}}
+
+	for _, test := range tests {
+		txHash := test.subject.Hash()
+		txDesc := harness.txPool.pool[*txHash]
+		miningView := harness.txPool.MiningView()
+
+		// Make sure transaction is not rejected between view instances.
+		if miningView.IsRejected(txHash) {
+			t.Fatalf("%v: expected test subject txn %v to not be rejected",
+				test.name, *txHash)
+		}
+
+		// Ensure stats are calculated before retrieving ancestors.
+		initialStat, hasStats := miningView.AncestorStats(txHash)
+		if !hasStats {
+			t.Fatalf("%v: expected mining view to have ancestor stats",
+				test.name)
+		}
+
+		if test.expectedAncestorFees != initialStat.Fees {
+			t.Fatalf("%v: expected subject txn to have bundle fees %v, got %v",
+				test.name, test.expectedAncestorFees, initialStat.Fees)
+		}
+
+		if test.expectedSizeBytes != initialStat.SizeBytes {
+			t.Fatalf("%v: expected subject txn to have SizeBytes=%v, got %v",
+				test.name, test.expectedSizeBytes, initialStat.SizeBytes)
+		}
+
+		if test.expectedSigOps != initialStat.TotalSigOps {
+			t.Fatalf("%v: expected subject txn to have SigOps=%v, got %v",
+				test.name, test.expectedSigOps, initialStat.TotalSigOps)
+		}
+
+		if len(test.ancestors) != initialStat.NumAncestors {
+			t.Fatalf("%v: expected subject txn to have NumAncestors=%v, got %v",
+				test.name, len(test.ancestors), initialStat.NumAncestors)
+		}
+
+		// Retrieving ancestors will cause the cached stats to be updated.
+		ancestors := miningView.Ancestors(txHash)
+		stat, _ := miningView.AncestorStats(txHash)
+
+		if initialStat == stat {
+			t.Fatalf("%v: expected ancestor stats reference to have changed "+
+				"after retrieving list of ancestors", test.name)
+		}
+
+		// Get snapshot of transaction relationships as they exist in the pool.
+		descendants := harness.txPool.miningView.txGraph.descendants(txHash)
+
+		if len(test.ancestors) != len(ancestors) {
+			t.Fatalf("%v: expected subject txn to have %v ancestors, got %v",
+				test.name, len(test.ancestors), len(ancestors))
+		}
+
+		if len(ancestors) != stat.NumAncestors {
+			t.Fatalf("%v: expected subject txn to have NumAncestors=%v, got %v",
+				test.name, len(ancestors), stat.NumAncestors)
+		}
+
+		for _, descendantHash := range descendants {
+			if _, exist := test.descendants[*descendantHash]; !exist {
+				t.Fatalf("%v: expected subject txn to have descendant=%v",
+					test.name, descendantHash)
+			}
+		}
+
+		// Ensure that transactions have a valid order, and that one of the
+		// test's orderedAncestors matches ancestors returned from the view
+		// exactly.
+		exactMatch := true
+		for _, ancestorGroups := range test.orderedAncestors {
+			exactMatch = true
+			for index, ancestor := range ancestorGroups {
+				if *ancestors[index].Tx.Hash() != *ancestor.Hash() {
+					exactMatch = false
+					break
+				}
+			}
+
+			if exactMatch {
+				break
+			}
+		}
+
+		if !exactMatch {
+			t.Fatalf("%v: subject txn ancestors returned out of order",
+				test.name)
+		}
+
+		if miningView.HasParents(txHash) && len(test.ancestors) == 0 {
+			t.Fatalf("%v: expected subject txn to not have 0 parents, got %v",
+				test.name, len(test.ancestors))
+		}
+
+		// Make sure all parents are valid outpoints from this tx
+		txInputHashes := make(map[chainhash.Hash]struct{})
+		for _, outpoint := range test.subject.MsgTx().TxIn {
+			txInputHashes[outpoint.PreviousOutPoint.Hash] = struct{}{}
+		}
+
+		for _, parentDesc := range miningView.Parents(txHash) {
+			if _, exist := txInputHashes[*parentDesc.Tx.Hash()]; !exist {
+				t.Fatalf("%v: test subject %v has invalid parent %v",
+					test.name, *txHash, *parentDesc.Tx.Hash())
+			}
+		}
+
+		// if the transaction has children, make sure it's no longer the
+		// child's ancestor after removal.
+		removalMiningView := harness.txPool.MiningView()
+		if children := removalMiningView.Children(txHash); len(children) > 0 {
+			removalMiningView.Remove(txHash, false)
+			child := children[0]
+			siblings := removalMiningView.Parents(child.Tx.Hash())
+
+			// Make sure ancestor stats have not changed.
+			oldStat, hasStats := miningView.AncestorStats(child.Tx.Hash())
+			if !hasStats {
+				t.Fatalf("%v: Expected ancestor stats to be available",
+					test.name)
+			}
+
+			newStat, hasStats := removalMiningView.AncestorStats(
+				child.Tx.Hash())
+			if !hasStats {
+				t.Fatalf("%v: Expected ancestor stats to be available",
+					test.name)
+			}
+
+			if *oldStat != *newStat {
+				t.Fatalf("%v: expected test subject's child bundle stats to "+
+					"remain unchanged.", test.name)
+			}
+
+			for _, sibling := range siblings {
+				if *sibling.Tx.Hash() == *txHash {
+					t.Fatalf("%v: expected test subject to no longer be an "+
+						"ancestor of %v after being removed from the view.",
+						test.name, *sibling.Tx.Hash())
+				}
+			}
+		}
+
+		// reset the mining view after removing tx
+		// ensure all descendants have the fee removed for this transaction
+		// when calling Remove with option to do so.
+		removalMiningView = harness.txPool.MiningView()
+		removalMiningView.Remove(txHash, true)
+
+		for _, descendant := range descendants {
+			oldStat, hasStats := miningView.AncestorStats(descendant)
+			if !hasStats {
+				t.Fatalf("%v: Expected ancestor stats to be available",
+					test.name)
+			}
+
+			newStat, hasStats := removalMiningView.AncestorStats(descendant)
+			if !hasStats {
+				t.Fatalf("%v: Expected ancestor stats to be available",
+					test.name)
+			}
+
+			expectedFee := oldStat.Fees - txDesc.Fee
+			expectedSigOps := oldStat.TotalSigOps - txDesc.TotalSigOps
+			expectedSizeBytes := oldStat.SizeBytes -
+				int64(txDesc.Tx.MsgTx().SerializeSize())
+			expectedNumAncestors := oldStat.NumAncestors - 1
+
+			if newStat.Fees != expectedFee {
+				t.Fatalf("%v: expected descendant to have adjusted "+
+					"Fees=%v, but got %v", test.name, expectedFee, newStat.Fees)
+			}
+
+			if newStat.TotalSigOps != expectedSigOps {
+				t.Fatalf("%v: expected descendant to have adjusted "+
+					"NumSigOps=%v, but got %v", test.name,
+					expectedSigOps, newStat.TotalSigOps)
+			}
+
+			if newStat.SizeBytes != expectedSizeBytes {
+				t.Fatalf("%v: expected descendant to have adjusted "+
+					"SizeBytes=%v, but got %v", test.name,
+					expectedSizeBytes, newStat.SizeBytes)
+			}
+
+			if newStat.NumAncestors != expectedNumAncestors {
+				t.Fatalf("%v: expected descendant to have "+
+					"NumAncestors=%v, but got %v", test.name,
+					expectedNumAncestors, newStat.NumAncestors)
+			}
+		}
+
+		miningView.Reject(txHash)
+
+		if !miningView.IsRejected(txHash) {
+			t.Fatalf("%v: expected test subject txn %v to be rejected",
+				test.name, *txHash)
+		}
+
+		// Rejecting a transaction rejects all descendant transactions
+		// and removes the tx from the graph.
+		if ancestors := miningView.Ancestors(txHash); len(ancestors) > 0 {
+			t.Fatalf("%v: expected subject txn to have 0 ancestors, got %v",
+				test.name, len(ancestors))
+		}
+
+		for _, descendant := range descendants {
+			if !miningView.IsRejected(descendant) {
+				t.Fatalf("%v: expected descendant txn %v to be rejected.",
+					test.name, *descendant)
+			}
+		}
+	}
+
+	// Create mining view prior to remove txn from pool.
+	miningViewSnapshot := harness.txPool.MiningView()
+
+	// Remove txC and its descendants from the mempool.
+	harness.txPool.RemoveTransaction(txC, true, true)
+
+	// Add a new transaction to the mempool.
+	_, err = harness.txPool.MaybeAcceptTransaction(txF, true, false)
+	if err != nil {
+		t.Fatalf("AddTxnToPool: failed to accept valid transaction %v",
+			err)
+	}
+
+	miningView := harness.txPool.MiningView()
+	addedTxns := miningView.Children(txD.Hash())
+	if len(addedTxns) != 1 {
+		t.Fatalf("AddTxnToPool: expected txD to have exactly 1 child, got %v",
+			len(addedTxns))
+	}
+
+	if addedTxns[0].Tx != txF {
+		t.Fatalf("AddTxnToPool: expected txF to exist in the mining view.")
+	}
+
+	// Ensure newly added transaction not added to the mining view snapshot.
+	if len(miningViewSnapshot.Children(txD.Hash())) != 0 {
+		t.Fatalf("AddTxnToPool: expected txD to not have descendants in " +
+			"snapshot mining view.")
+	}
+
+	if len(miningView.Children(txC.Hash())) != 0 {
+		t.Fatalf("RemoveTxnFromPool: expected txC to not have descendents " +
+			"after being removed from the mempool.")
+	}
+
+	// Ensure tx and children are still accessible in cloned view.
+	if len(miningViewSnapshot.Children(txC.Hash())) != 1 {
+		t.Fatalf("RemoveTxnFromPool: expected txC to exist in snapshot view " +
+			"even though it was removed from the mempool.")
+	}
+}
diff --git a/internal/mempool/policy_test.go b/internal/mempool/policy_test.go
index 7a853248..32a3d187 100644
--- a/internal/mempool/policy_test.go
+++ b/internal/mempool/policy_test.go
@@ -7,6 +7,7 @@ package mempool
 
 import (
 	"bytes"
+	"errors"
 	"testing"
 	"time"
 
@@ -345,7 +346,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 		tx         wire.MsgTx
 		height     int64
 		isStandard bool
-		code       ErrorCode
+		err        error
 	}{
 		{
 			name: "Typical pay-to-pubkey-hash transaction",
@@ -370,7 +371,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 			},
 			height:     300000,
 			isStandard: false,
-			code:       ErrNonStandard,
+			err:        ErrNonStandard,
 		},
 		{
 			name: "Transaction version too high",
@@ -383,7 +384,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 			},
 			height:     300000,
 			isStandard: false,
-			code:       ErrNonStandard,
+			err:        ErrNonStandard,
 		},
 		{
 			name: "Transaction is not finalized",
@@ -400,7 +401,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 			},
 			height:     300000,
 			isStandard: false,
-			code:       ErrNonStandard,
+			err:        ErrNonStandard,
 		},
 		{
 			name: "Transaction size is too large",
@@ -417,7 +418,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 			},
 			height:     300000,
 			isStandard: false,
-			code:       ErrNonStandard,
+			err:        ErrNonStandard,
 		},
 		{
 			name: "Signature script size is too large",
@@ -435,7 +436,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 			},
 			height:     300000,
 			isStandard: false,
-			code:       ErrNonStandard,
+			err:        ErrNonStandard,
 		},
 		{
 			name: "Signature script that does more than push data",
@@ -453,7 +454,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 			},
 			height:     300000,
 			isStandard: false,
-			code:       ErrNonStandard,
+			err:        ErrNonStandard,
 		},
 		{
 			name: "Valid but non standard public key script",
@@ -469,7 +470,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 			},
 			height:     300000,
 			isStandard: false,
-			code:       ErrNonStandard,
+			err:        ErrNonStandard,
 		},
 		{
 			name: "More than four nulldata outputs",
@@ -497,7 +498,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 			},
 			height:     300000,
 			isStandard: false,
-			code:       ErrNonStandard,
+			err:        ErrNonStandard,
 		},
 		{
 			name: "Dust output",
@@ -513,7 +514,7 @@ func TestCheckTransactionStandard(t *testing.T) {
 			},
 			height:     300000,
 			isStandard: false,
-			code:       ErrDustOutput,
+			err:        ErrDustOutput,
 		},
 		{
 			name: "One nulldata output with 0 amount (standard)",
@@ -556,9 +557,9 @@ func TestCheckTransactionStandard(t *testing.T) {
 		}
 
 		// Ensure the error code is the expected one.
-		if !IsErrorCode(err, test.code) {
+		if !errors.Is(err, test.err) {
 			t.Errorf("checkTransactionStandard (%s): unexpected error -- got "+
-				"%v, want %v", test.name, err, test.code)
+				"%v, want %v", test.name, err, test.err)
 			continue
 		}
 	}
diff --git a/internal/mining/mining_test.go b/internal/mining/mining_test.go
index 1626ef9f..b4451b48 100644
--- a/internal/mining/mining_test.go
+++ b/internal/mining/mining_test.go
@@ -36,6 +36,7 @@ func TestStakeTxFeePrioHeap(t *testing.T) {
 		{feePerKB: 10000, txType: stake.TxTypeRegular, priority: 0}, // Higher fee, smaller prio
 		{feePerKB: 0, txType: stake.TxTypeRegular, priority: 10000}, // Higher prio, lower fee
 	}
+
 	ph := newTxPriorityQueue((numElements + numEdgeConditionElements), txPQByStakeAndFee)
 
 	// Add random data in addition to the edge conditions already manually
@@ -46,7 +47,7 @@ func TestStakeTxFeePrioHeap(t *testing.T) {
 			randPrio := rand.Float64() * 100
 			randFeePerKB := rand.Float64() * 10
 			testItems = append(testItems, &txPrioItem{
-				tx:       nil,
+				txDesc:   nil,
 				txType:   randType,
 				feePerKB: randFeePerKB,
 				priority: randPrio,
@@ -58,7 +59,7 @@ func TestStakeTxFeePrioHeap(t *testing.T) {
 
 	// Test sorting by stake and fee per KB.
 	last := &txPrioItem{
-		tx:       nil,
+		txDesc:   nil,
 		txType:   stake.TxTypeSSGen,
 		priority: 10000.0,
 		feePerKB: 10000.0,
@@ -83,7 +84,7 @@ func TestStakeTxFeePrioHeap(t *testing.T) {
 		randPrio := rand.Float64() * 100
 		randFeePerKB := rand.Float64() * 10
 		prioItem := &txPrioItem{
-			tx:       nil,
+			txDesc:   nil,
 			txType:   randType,
 			feePerKB: randFeePerKB,
 			priority: randPrio,
@@ -94,7 +95,7 @@ func TestStakeTxFeePrioHeap(t *testing.T) {
 	// Test sorting with fees per KB for high stake priority, then
 	// priority for low stake priority.
 	last = &txPrioItem{
-		tx:       nil,
+		txDesc:   nil,
 		txType:   stake.TxTypeSSGen,
 		priority: 10000.0,
 		feePerKB: 10000.0,
diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go
index 10e4e382..a369c8e2 100644
--- a/internal/rpcserver/rpcserverhandlers_test.go
+++ b/internal/rpcserver/rpcserverhandlers_test.go
@@ -5693,7 +5693,7 @@ func TestHandleGetWork(t *testing.T) {
 		mockSyncManager: func() *testSyncManager {
 			syncManager := defaultMockSyncManager()
 			syncManager.submitBlockErr = blockchain.RuleError{
-				ErrorCode:   blockchain.ErrDuplicateBlock,
+				Err:         blockchain.ErrDuplicateBlock,
 				Description: "Duplicate Block",
 			}
 			return syncManager