Repository: decred/dcrd
Pull request #2540 (2021-01-01 16:08:19)

multi: Rework utxoset/view to use outpoints.
**NOTE: This contains a database migration, so if you plan to test it, please make a copy of your data directory first.**

---

This modifies the utxoset in the database and related `UtxoViewpoint` to store and work with unspent transaction outputs on a per-output basis instead of at a transaction level.  This is a port of https://github.com/btcsuite/btcd/pull/1045 with some significant differences due to the stake tree and transactions in Decred.

The primary motivation is to simplify the code, pave the way for a utxo cache, and generally focus on optimizing runtime performance.

The tradeoff is that this approach does somewhat increase the size of the serialized utxoset since it means that the transaction hash is duplicated for each output as a part of the key and some additional details are duplicated in each output.  The details duplicated in each output include flags encoded into a single byte that specify whether the containing transaction is a coinbase, whether the containing transaction has an expiry, and the transaction type.  Additionally, the containing block height and index are stored in each output.

However, in practice, the size difference isn't all that large, disk space is relatively cheap, certainly cheaper than memory, and it is much more important to provide more efficient runtime operation since that is the ultimate purpose of the daemon.

While performing this conversion, it also simplifies the code to remove the transaction version information from the utxoset as well as the spend journal.  The logic for only serializing it under certain circumstances is complicated, and it was only used for the `gettxout` RPC, where it has already been removed.

The utxo set and spend journal in the database are automatically migrated to the new format with this commit and it is possible to interrupt and resume the migration process.

Finally, it also updates all references and tests that previously dealt with transaction hashes to use outpoints instead.

An overview of the changes are as follows:

- Remove transaction version from both spent and unspent output entries
  - Update utxo serialization format to exclude the version
  - Update spend journal serialization format to exclude the version
- Convert `UtxoEntry` to represent a specific utxo instead of a transaction with all remaining utxos
  - Optimize for memory usage with an eye toward a utxo cache
    - Combine fields such as whether the containing transaction is a coinbase, whether the containing transaction has an expiry, and the transaction type, into a single byte
    - Align entry fields to eliminate extra padding since ultimately there will be a lot of these in memory
    - Introduce a free list for serializing an outpoint to the database key format to significantly reduce pressure on the GC
  - Update entries to be keyed by a `<hash><tree><output index>` outpoint rather than just a tx hash
  - Update all related functions that previously dealt with transaction hashes to accept outpoints instead
  - Update all callers accordingly
  - Only add individually requested outputs from the mempool when constructing a mempool view
- Modify the spend journal to always store the encoded flags with every spent txout
  - Combine fields such as whether the containing transaction is a coinbase, whether the containing transaction has an expiry, and the transaction type into a single byte
    - Use 4 bits instead of 3 for the transaction type to be consistent with utxos. The extra bit was already unused so this doesn't take any additional space
  - Remove the fully spent flag
- Introduce `ticketMinOuts` in place of `stakeExtra`
  - Renamed `stakeExtra` as `ticketMinOuts` and updated all comments to make the purpose of the field clearer
  - Only store `ticketMinOuts` for ticket submission outputs
  - Add `TicketMinimalOutputs` function on `UtxoEntry` in place of `ConvertUtxosToMinimalOutputs`
- Always decompress data loaded from the database now that a utxo entry only consists of a specific output
- Introduce upgrade code to migrate the utxo set and spend journal to the new format
  - Update current database version to 9
  - Update current utxo set version to 3
  - Update current spend journal version to 3
  - Introduce the ability to run upgrades after the block index has been loaded
- Update all tests to expect the correct encodings, remove tests that no longer apply, and add new ones for the new expected behavior
  - Convert old tests for the legacy utxo format deserialization code to test the new function that is used during upgrade
- Introduce a few new functions on `UtxoViewpoint`
  - `AddTxOut` for adding an individual txout versus all of them
  - `addTxOut` to handle the common code between the new `AddTxOut` and existing `AddTxOuts`
  - `RemoveEntry` for removing an individual txout
- Remove the `ErrDiscordantTxTree` error
  - Since utxos are now retrieved using an outpoint, which includes the tree, it is no longer possible to hit this error path

---

A couple other notes on the implementation:

- Unfortunately, it wasn't really possible to break this into smaller working commits, so these changes are all in one large commit.
- The spend journal migration does take awhile to run (~20 mins, due to the fact that it needs to read all block entries).

---

**Part of https://github.com/decred/dcrd/issues/1145.**

rstaudt2: I made an update to store `ticketMinOuts` in all ticket outputs rather than only in ticket submission outputs.  This was required because the legacy spend journal stored that data for every ticket output, so to maintain backward compatibility, it needs to remain the same (otherwise, there will be an issue in the unlikely case of deep reorgs). 

I am going to start taking a look at implementing a migration that recreates the spend journal data in the new format from scratch (in a separate PR) since I'd prefer not to have to keep the legacy handling code around for long.
rstaudt2: Found one more issue related to processing deep reorgs with the legacy stxo handling ([diff](https://github.com/decred/dcrd/compare/87a1d67131e1ab09b95a5752a2fa11380d22fba6..335e895109a67d198aaf1973f982ce77ebbc61ef)).  This is actually a bug in the [btcd code](https://github.com/btcsuite/btcd/blob/6bd4c64a54faeb343d5e3c1ee5802450a291787f/blockchain/utxoviewpoint.go#L425) as well - it is fetching a utxo using the tx hash rather than the hash of the input transaction (though it doesn't really matter, since that code path would never be hit at this point since its only run when encountering legacy stxo entires).

I should be able to get the stxo migration in place relatively quickly, which will avoid this case altogether (though unfortunately, since it needs to read all historical blocks/txns, it will likely take in the range of 10-20 mins to complete).
rstaudt2: Rebased to `master` and also made the comment updates that @sefbkn pointed out.

Also, as a heads up, I have the stxo migration ready and will be pushing it to this PR shortly.  Sorry in advance for the churn, but I think it is worth bringing into this PR directly because it avoids having another utxo migration and completely removes the legacy handling cases.  I'll call out the changes/diff when I push that up.
davecgh: > Also, as a heads up, I have the stxo migration ready and will be pushing it to this PR shortly. Sorry in advance for the churn, but I think it is worth bringing into this PR directly because it avoids having another utxo migration and completely removes the legacy handling cases. I'll call out the changes/diff when I push that up.

Agreed.  I was going to suggest this myself if it was done prior to this one being finalized, so glad to see you're of the same mindset.

rstaudt2: The spend journal migration has now been added ([diff](https://github.com/decred/dcrd/compare/d83712db11c84cf17dd605e079cd4498e4e11777..d138150ee6045867903221189b85946e58925b89)).  The updates include the following:

- Added a `upgradeDBPostBlockIndexLoad` function to run migrations after the block index has been loaded.
- Added the `migrateSpendJournalVersion2To3` migration.  It takes about ~20 mins to run since it has to read all block entries.
- Updated ticket minimal outputs to only be stored in ticket _submission_ outputs (since that could now be handled properly with the spend journal migration in place).
- Removed the reserved field from the spend journal entries since it is no longer needed for backward compatibility.
- Removed the spent flag from the spend journal entries since it is no longer needed for backward compatibility.
- Removed the code that was handling legacy v2 spend journal entries since they are now migrated.
rstaudt2: > The spend journal migration has now been added ([diff](https://github.com/decred/dcrd/compare/d83712db11c84cf17dd605e079cd4498e4e11777..d138150ee6045867903221189b85946e58925b89)). The updates include the following:
> 
>     * Added a `upgradeDBPostBlockIndexLoad` function to run migrations after the block index has been loaded.
> 
>     * Added the `migrateSpendJournalVersion2To3` migration.  It takes about ~20 mins to run since it has to read all block entries.
> 
>     * Updated ticket minimal outputs to only be stored in ticket _submission_ outputs (since that could now be handled properly with the spend journal migration in place).
> 
>     * Removed the reserved field from the spend journal entries since it is no longer needed for backward compatibility.
> 
>     * Removed the spent flag from the spend journal entries since it is no longer needed for backward compatibility.
> 
>     * Removed the code that was handling legacy v2 spend journal entries since they are now migrated.

I also tested a deep reorg (invalidated/reconsidered a block thousands of blocks deep into the migrated data) to ensure that the migrated spent journal entries could be resurrected properly. 
rstaudt2: > The only outstanding issue as far as I can tell is the migration code issue surrounding side chain blocks. Once that is in, I'll give it another round of review and testing and assuming that goes well, this will be ready.

This should be resolved now, did further testing around side chain blocks for the migration as well as invalidating/reconsidering the main chain block at the same height as a side chain block.


Patch:
diff --git a/blockchain/chainio.go b/blockchain/chainio.go
index d63f26ba..fab11e30 100644
--- a/blockchain/chainio.go
+++ b/blockchain/chainio.go
@@ -12,7 +12,7 @@ import (
 	"errors"
 	"fmt"
 	"math/big"
-	"sort"
+	"sync"
 	"time"
 
 	"github.com/decred/dcrd/blockchain/stake/v4"
@@ -28,7 +28,7 @@ import (
 const (
 	// currentDatabaseVersion indicates what the current database
 	// version is.
-	currentDatabaseVersion = 8
+	currentDatabaseVersion = 9
 
 	// currentBlockIndexVersion indicates what the current block index
 	// database version.
@@ -74,11 +74,11 @@ var (
 
 	// spendJournalBucketName is the name of the db bucket used to house
 	// transactions outputs that are spent in each block.
-	spendJournalBucketName = []byte("spendjournalv2")
+	spendJournalBucketName = []byte("spendjournalv3")
 
 	// utxoSetBucketName is the name of the db bucket used to house the unspent
 	// transaction output set.
-	utxoSetBucketName = []byte("utxosetv2")
+	utxoSetBucketName = []byte("utxosetv3")
 
 	// blockIndexBucketName is the name of the db bucket used to house the block
 	// index which consists of metadata for all known blocks both in the main
@@ -249,15 +249,6 @@ func readDeserializeSizeOfMinimalOutputs(serialized []byte) (int, error) {
 	return offset, nil
 }
 
-// ConvertUtxosToMinimalOutputs converts the contents of a UTX to a series of
-// minimal outputs. It does this so that these can be passed to stake subpackage
-// functions, where they will be evaluated for correctness.
-func ConvertUtxosToMinimalOutputs(entry *UtxoEntry) []*stake.MinimalOutput {
-	minOuts, _ := deserializeToMinimalOutputs(entry.stakeExtra)
-
-	return minOuts
-}
-
 // -----------------------------------------------------------------------------
 // The block index consists of an entry for every known block.  It consists of
 // information such as the block header and information about votes.
@@ -481,7 +472,7 @@ func dbMaybeStoreBlock(dbTx database.Tx, block *dcrutil.Block) error {
 // The serialized format is:
 //
 //   [<flags><script version><compressed pk script>],...
-//   OPTIONAL: [<txVersion>]
+//   OPTIONAL: <ticket min outs>
 //
 //   Field                Type           Size
 //   flags                VLQ            byte
@@ -489,55 +480,65 @@ func dbMaybeStoreBlock(dbTx database.Tx, block *dcrutil.Block) error {
 //   pkScript             VLQ+[]byte     variable
 //
 //   OPTIONAL
-//     txVersion          VLQ            variable
-//     stakeExtra         []byte         variable
-//
-// The serialized flags code format is:
-//   bit  0   - containing transaction is a coinbase
-//   bit  1   - containing transaction has an expiry
-//   bits 2-4 - transaction type
-//   bit  5   - unused
-//   bit  6   - is fully spent
-//   bit  7   - unused
+//     ticketMinOuts      []byte         variable
 //
-// The stake extra field contains minimally encoded outputs for all
-// consensus-related outputs in the stake transaction. It is only
-// encoded for tickets.
+// The serialized flags format is:
+//   bit  0     - containing transaction is a coinbase
+//   bit  1     - containing transaction has an expiry
+//   bits 2-5   - transaction type
+//   bits 6-7   - unused
 //
-//   NOTE: The transaction version and flags are only encoded when the spent
-//   txout was the final unspent output of the containing transaction.
-//   Otherwise, the header code will be 0 and the version is not serialized at
-//   all. This is done because that information is only needed when the utxo
-//   set no longer has it.
+// The ticket min outs field contains minimally encoded outputs for all outputs
+// of a ticket transaction. It is only encoded for ticket submission outputs.
 //
-// Example:
-//   TODO
 // -----------------------------------------------------------------------------
 
 // spentTxOut contains a spent transaction output and potentially additional
 // contextual information such as whether or not it was contained in a coinbase
-// transaction, the txVersion of the transaction it was contained in, and which
-// block height the containing transaction was included in.  As described in
-// the comments above, the additional contextual information will only be valid
-// when this spent txout is spending the last unspent output of the containing
-// transaction.
+// transaction, whether or not the containing transaction has an expiry, and the
+// transaction type.
 //
 // The struct is aligned for memory efficiency.
 type spentTxOut struct {
-	pkScript   []byte // The public key script for the output.
-	stakeExtra []byte // Extra information for the staking system.
-
-	amount        int64        // The amount of the output.
-	txType        stake.TxType // The stake type of the transaction.
-	height        uint32       // Height of the block containing the tx.
-	index         uint32       // Index in the block of the transaction.
-	scriptVersion uint16       // The version of the scripting language.
-	txVersion     uint16       // The version of creating tx.
-
-	txFullySpent bool // Whether or not the transaction is fully spent.
-	isCoinBase   bool // Whether creating tx is a coinbase.
-	hasExpiry    bool // The expiry of the creating tx.
-	compressed   bool // Whether or not the script is compressed.
+	amount   int64
+	pkScript []byte
+
+	// ticketMinOuts is the minimal outputs for the ticket transaction that the
+	// output is contained in.  This is only stored in ticket submission outputs
+	// and is nil for all other output types.
+	//
+	// Note that this is using a pointer rather than a slice in order to occupy
+	// less space when it is nil.  It is nil in the vast majority of entries, so
+	// this provides a significant overall reduction in memory usage.
+	ticketMinOuts *ticketMinimalOutputs
+
+	blockHeight   uint32
+	blockIndex    uint32
+	scriptVersion uint16
+
+	// packedFlags contains additional info about the output as defined by
+	// txOutFlags.  This approach is used in order to reduce memory usage since
+	// there will be a lot of these in memory.
+	packedFlags txOutFlags
+}
+
+// IsCoinBase returns whether or not the output was contained in a coinbase
+// transaction.
+func (stxo *spentTxOut) IsCoinBase() bool {
+	return stxo.packedFlags&txOutFlagCoinBase == txOutFlagCoinBase
+}
+
+// HasExpiry returns whether or not the output was contained in a transaction
+// that included an expiry.
+func (stxo *spentTxOut) HasExpiry() bool {
+	return stxo.packedFlags&txOutFlagHasExpiry == txOutFlagHasExpiry
+}
+
+// TransactionType returns the type of the transaction that the output is
+// contained in.
+func (stxo *spentTxOut) TransactionType() stake.TxType {
+	txType := (stxo.packedFlags & txOutFlagTxTypeBitmask) >> txOutFlagTxTypeShift
+	return stake.TxType(txType)
 }
 
 // spentTxOutSerializeSize returns the number of bytes it would take to
@@ -546,21 +547,16 @@ type spentTxOut struct {
 // because they're already encoded into the transactions, so skip them when
 // determining the serialization size.
 func spentTxOutSerializeSize(stxo *spentTxOut) int {
-	flags := encodeFlags(stxo.isCoinBase, stxo.hasExpiry, stxo.txType,
-		stxo.txFullySpent)
+	flags := encodeFlags(stxo.IsCoinBase(), stxo.HasExpiry(),
+		stxo.TransactionType())
 	size := serializeSizeVLQ(uint64(flags))
 
-	// false below indicates that the txOut does not specify an amount.
+	const hasAmount = false
 	size += compressedTxOutSize(uint64(stxo.amount), stxo.scriptVersion,
-		stxo.pkScript, currentCompressionVersion, stxo.compressed, false)
-
-	// The transaction was fully spent, so we need to store some extra
-	// data for UTX resurrection.
-	if stxo.txFullySpent {
-		size += serializeSizeVLQ(uint64(stxo.txVersion))
-		if stxo.txType == stake.TxTypeSStx {
-			size += len(stxo.stakeExtra)
-		}
+		stxo.pkScript, currentCompressionVersion, hasAmount)
+
+	if stxo.ticketMinOuts != nil {
+		size += len(stxo.ticketMinOuts.data)
 	}
 
 	return size
@@ -571,94 +567,66 @@ func spentTxOutSerializeSize(stxo *spentTxOut) int {
 // be at least large enough to handle the number of bytes returned by the
 // spentTxOutSerializeSize function or it will panic.
 func putSpentTxOut(target []byte, stxo *spentTxOut) int {
-	flags := encodeFlags(stxo.isCoinBase, stxo.hasExpiry, stxo.txType,
-		stxo.txFullySpent)
+	flags := encodeFlags(stxo.IsCoinBase(), stxo.HasExpiry(),
+		stxo.TransactionType())
 	offset := putVLQ(target, uint64(flags))
 
-	// false below indicates that the txOut does not specify an amount.
+	const hasAmount = false
 	offset += putCompressedTxOut(target[offset:], 0, stxo.scriptVersion,
-		stxo.pkScript, currentCompressionVersion, stxo.compressed, false)
-
-	// The transaction was fully spent, so we need to store some extra
-	// data for UTX resurrection.
-	if stxo.txFullySpent {
-		offset += putVLQ(target[offset:], uint64(stxo.txVersion))
-		if stxo.txType == stake.TxTypeSStx {
-			copy(target[offset:], stxo.stakeExtra)
-			offset += len(stxo.stakeExtra)
-		}
+		stxo.pkScript, currentCompressionVersion, hasAmount)
+
+	if stxo.ticketMinOuts != nil {
+		copy(target[offset:], stxo.ticketMinOuts.data)
+		offset += len(stxo.ticketMinOuts.data)
 	}
+
 	return offset
 }
 
 // decodeSpentTxOut decodes the passed serialized stxo entry, possibly followed
 // by other data, into the passed stxo struct.  It returns the number of bytes
 // read.
-//
-// Since the serialized stxo entry does not contain the height, version, or
-// coinbase flag of the containing transaction when it still has utxos, the
-// caller is responsible for passing in the containing transaction version in
-// that case.  The provided version is ignore when it is serialized as a part of
-// the stxo.
-//
-// An error will be returned if the version is not serialized as a part of the
-// stxo and is also not provided to the function.
-func decodeSpentTxOut(serialized []byte, stxo *spentTxOut, amount int64, height uint32, index uint32) (int, error) {
+func decodeSpentTxOut(serialized []byte, stxo *spentTxOut, amount int64,
+	height uint32, index uint32, txOutIndex uint32) (int, error) {
+
 	// Deserialize the flags.
-	flags, offset := deserializeVLQ(serialized)
-	if offset == 0 {
-		return 0, errDeserialize("unexpected end of data during " +
-			"decoding (flags)")
+	flags, bytesRead := deserializeVLQ(serialized)
+	offset := bytesRead
+	if offset >= len(serialized) {
+		return offset, errDeserialize("unexpected end of data after flags")
 	}
 
 	// Decode the compressed txout. We pass false for the amount flag,
 	// since in Decred we only need pkScript at most due to fraud proofs
 	// already storing the decompressed amount.
-	_, scriptVersion, compScript, bytesRead, err :=
-		decodeCompressedTxOut(serialized[offset:], currentCompressionVersion,
-			false)
+	_, scriptVersion, script, bytesRead, err :=
+		decodeCompressedTxOut(serialized[offset:], currentCompressionVersion, false)
 	offset += bytesRead
 	if err != nil {
 		return offset, errDeserialize(fmt.Sprintf("unable to decode "+
 			"txout: %v", err))
 	}
-	stxo.scriptVersion = scriptVersion
-	stxo.amount = amount
-	stxo.pkScript = compScript
-	stxo.compressed = true
-	stxo.height = height
-	stxo.index = index
-
-	// Deserialize the containing transaction if the flags indicate that
-	// the transaction has been fully spent.
-	if decodeFlagsFullySpent(byte(flags)) {
-		isCoinBase, hasExpiry, txType, _ := decodeFlags(byte(flags))
-
-		stxo.isCoinBase = isCoinBase
-		stxo.hasExpiry = hasExpiry
-		stxo.txType = txType
-		stxo.txFullySpent = true
-
-		txVersion, bytesRead := deserializeVLQ(serialized[offset:])
-		if bytesRead == 0 {
-			return offset, errDeserialize("unexpected end of " +
-				"data during decoding (tx version)")
-		}
-		offset += bytesRead
 
-		stxo.txVersion = uint16(txVersion)
+	// Populate the stxo.
+	stxo.amount = amount
+	stxo.pkScript = script
+	stxo.blockHeight = height
+	stxo.blockIndex = index
+	stxo.scriptVersion = scriptVersion
+	stxo.packedFlags = txOutFlags(flags)
 
-		if stxo.txType == stake.TxTypeSStx {
-			sz, err := readDeserializeSizeOfMinimalOutputs(serialized[offset:])
-			if err != nil {
-				return offset + sz, errDeserialize(fmt.Sprintf("unable to decode "+
-					"ticket outputs: %v", err))
-			}
-			stakeExtra := make([]byte, sz)
-			copy(stakeExtra, serialized[offset:offset+sz])
-			stxo.stakeExtra = stakeExtra
-			offset += sz
+	// Copy the minimal outputs if this was a ticket submission output.
+	if isTicketSubmissionOutput(stxo.TransactionType(), txOutIndex) {
+		sz, err := readDeserializeSizeOfMinimalOutputs(serialized[offset:])
+		if err != nil {
+			return offset + sz, errDeserialize(fmt.Sprintf("unable to decode "+
+				"ticket outputs: %v", err))
 		}
+		stxo.ticketMinOuts = &ticketMinimalOutputs{
+			data: make([]byte, sz),
+		}
+		copy(stxo.ticketMinOuts.data, serialized[offset:offset+sz])
+		offset += sz
 	}
 
 	return offset, nil
@@ -717,25 +685,8 @@ func deserializeSpendJournalEntry(serialized []byte, txns []*wire.MsgTx, isTreas
 			stxo := &stxos[stxoIdx]
 			stxoIdx--
 
-			// Get the transaction version for the stxo based on
-			// whether or not it should be serialized as a part of
-			// the stxo.  Recall that it is only serialized when the
-			// stxo spends the final utxo of a transaction.  Since
-			// they are deserialized in reverse order, this means
-			// the first time an entry for a given containing tx is
-			// encountered that is not already in the utxo view it
-			// must have been the final spend and thus the extra
-			// data will be serialized with the stxo.  Otherwise,
-			// the version must be pulled from the utxo entry.
-			//
-			// Since the view is not actually modified as the stxos
-			// are read here and it's possible later entries
-			// reference earlier ones, an inflight map is maintained
-			// to detect this case and pull the tx version from the
-			// entry that contains the version information as just
-			// described.
 			n, err := decodeSpentTxOut(serialized[offset:], stxo, txIn.ValueIn,
-				txIn.BlockHeight, txIn.BlockIndex)
+				txIn.BlockHeight, txIn.BlockIndex, txIn.PreviousOutPoint.Index)
 			offset += n
 			if err != nil {
 				return nil, errDeserialize(fmt.Sprintf("unable "+
@@ -850,181 +801,158 @@ func dbRemoveSpendJournalEntry(dbTx database.Tx, blockHash *chainhash.Hash) erro
 
 // -----------------------------------------------------------------------------
 // The unspent transaction output (utxo) set consists of an entry for each
-// transaction which contains a utxo serialized using a format that is highly
-// optimized to reduce space using domain specific compression algorithms.  This
-// format is a slightly modified version of the format used in Bitcoin Core.
+// unspent output using a format that is optimized to reduce space using domain
+// specific compression algorithms.
 //
-// The serialized format is:
+// Each entry is keyed by an outpoint as specified below.  It is important to
+// note that the key encoding uses a VLQ, which employs an MSB encoding so
+// iteration of utxos when doing byte-wise comparisons will produce them in
+// order.
 //
-//   <version><height><header code><unspentness bitmap>[<compressed txouts>,...]
+// The serialized key format is:
+//
+//   <hash><tree><output index>
 //
-//   Field                 Type     Size
-//   transaction version   VLQ      variable
-//   block height          VLQ      variable
-//   block index           VLQ      variable
-//   flags                 VLQ      variable (currently 1 byte)
-//   header code           VLQ      variable
-//   unspentness bitmap    []byte   variable
-//   compressed txouts
+//   Field                Type             Size
+//   hash                 chainhash.Hash   chainhash.HashSize
+//   tree                 VLQ              variable
+//   output index         VLQ              variable
+//
+// The serialized value format is:
+//
+//   <block height><block index><flags><compressed txout>
+//   OPTIONAL: [<ticket min outs>]
+//
+//   Field                Type     Size
+//   block height         VLQ      variable
+//   block index          VLQ      variable
+//   flags                VLQ      variable
+//   compressed txout
 //     compressed amount   VLQ      variable
-//     compressed version  VLQ      variable
+//     script version      VLQ      variable
 //     compressed script   []byte   variable
-//   stakeExtra            []byte   variable
-//
-// The serialized flags code format is:
-//   bit  0   - containing transaction is a coinbase
-//   bit  1   - containing transaction has an expiry
-//   bits 2-4 - transaction type
-//   bit  5   - unused
-//   bit  6   - is fully spent
-//   bit  7   - unused
 //
-// The serialized header code format is:
-//   bit 0 - output zero is unspent
-//   bit 1 - output one is unspent
-//   bits 2-x - number of bytes in unspentness bitmap.  When both bits 1 and 2
-//     are unset, it encodes N-1 since there must be at least one unspent
-//     output.
+//   OPTIONAL
+//     ticketMinOuts      []byte         variable
 //
-// The rationale for the header code scheme is as follows:
-//   - Transactions which only pay to a single output and a change output are
-//     extremely common, thus an extra byte for the unspentness bitmap can be
-//     avoided for them by encoding those two outputs in the low order bits.
-//   - Given it is encoded as a VLQ which can encode values up to 127 with a
-//     single byte, that leaves 4 bits to represent the number of bytes in the
-//     unspentness bitmap while still only consuming a single byte for the
-//     header code.  In other words, an unspentness bitmap with up to 120
-//     transaction outputs can be encoded with a single-byte header code.
-//     This covers the vast majority of transactions.
-//   - Encoding N-1 bytes when both bits 0 and 1 are unset allows an additional
-//     8 outpoints to be encoded before causing the header code to require an
-//     additional byte.
+// The serialized flags format is:
+//   bit  0     - containing transaction is a coinbase
+//   bit  1     - containing transaction has an expiry
+//   bits 2-5   - transaction type
+//   bits 6-7   - unused
 //
-// The stake extra field contains minimally encoded outputs for all
-// consensus-related outputs in the stake transaction. It is only
-// encoded for tickets.
+// The ticket min outs field contains minimally encoded outputs for all outputs
+// of a ticket transaction. It is only encoded for ticket submission outputs.
 //
-// Example 1: TODO
 // -----------------------------------------------------------------------------
 
-// utxoEntryHeaderCode returns the calculated header code to be used when
-// serializing the provided utxo entry and the number of bytes needed to encode
-// the unspentness bitmap.
-func utxoEntryHeaderCode(entry *UtxoEntry, highestOutputIndex uint32) (uint64, int, error) {
-	// The first two outputs are encoded separately, so offset the index
-	// accordingly to calculate the correct number of bytes needed to encode
-	// up to the highest unspent output index.
-	numBitmapBytes := int((highestOutputIndex + 6) / 8)
-
-	// As previously described, one less than the number of bytes is encoded
-	// when both output 0 and 1 are spent because there must be at least one
-	// unspent output.  Adjust the number of bytes to encode accordingly and
-	// encode the value by shifting it over 2 bits.
-	output0Unspent := !entry.IsOutputSpent(0)
-	output1Unspent := !entry.IsOutputSpent(1)
-	var numBitmapBytesAdjustment int
-	if !output0Unspent && !output1Unspent {
-		if numBitmapBytes == 0 {
-			return 0, 0, AssertError("attempt to serialize utxo " +
-				"header for fully spent transaction")
-		}
-		numBitmapBytesAdjustment = 1
-	}
-	headerCode := uint64(numBitmapBytes-numBitmapBytesAdjustment) << 2
+// maxUint32VLQSerializeSize is the maximum number of bytes a max uint32 takes
+// to serialize as a VLQ.
+var maxUint32VLQSerializeSize = serializeSizeVLQ(1<<32 - 1)
+
+// maxUint8VLQSerializeSize is the maximum number of bytes a max uint8 takes to
+// serialize as a VLQ.
+var maxUint8VLQSerializeSize = serializeSizeVLQ(1<<8 - 1)
+
+// outpointKeyPool defines a concurrent safe free list of byte slices used to
+// provide temporary buffers for outpoint database keys.
+var outpointKeyPool = sync.Pool{
+	New: func() interface{} {
+		b := make([]byte, chainhash.HashSize+maxUint8VLQSerializeSize+
+			maxUint32VLQSerializeSize)
+		return &b // Pointer to slice to avoid boxing alloc.
+	},
+}
 
-	// Set the output 0 and output 1 bits in the header code
-	// accordingly.
-	if output0Unspent {
-		headerCode |= 0x01 // bit 0
+// outpointKey returns a key suitable for use as a database key in the utxo set
+// while making use of a free list.  A new buffer is allocated if there are not
+// already any available on the free list.  The returned byte slice should be
+// returned to the free list by using the recycleOutpointKey function when the
+// caller is done with it _unless_ the slice will need to live for longer than
+// the caller can calculate such as when used to write to the database.
+func outpointKey(outpoint wire.OutPoint) *[]byte {
+	// A VLQ employs an MSB encoding, so they are useful not only to reduce the
+	// amount of storage space, but also so iteration of utxos when doing
+	// byte-wise comparisons will produce them in order.
+	key := outpointKeyPool.Get().(*[]byte)
+	tree := uint64(outpoint.Tree)
+	idx := uint64(outpoint.Index)
+	*key = (*key)[:chainhash.HashSize+serializeSizeVLQ(tree) +
+		+serializeSizeVLQ(idx)]
+	copy(*key, outpoint.Hash[:])
+	offset := chainhash.HashSize
+	offset += putVLQ((*key)[offset:], tree)
+	putVLQ((*key)[offset:], idx)
+	return key
+}
+
+// decodeOutpointKey decodes the passed serialized key into the passed outpoint.
+func decodeOutpointKey(serialized []byte, outpoint *wire.OutPoint) error {
+	if chainhash.HashSize >= len(serialized) {
+		return errDeserialize("unexpected length for serialized outpoint key")
 	}
-	if output1Unspent {
-		headerCode |= 0x02 // bit 1
+
+	// Deserialize the hash.
+	var hash chainhash.Hash
+	copy(hash[:], serialized[:chainhash.HashSize])
+	offset := chainhash.HashSize
+
+	// Deserialize the tree.
+	tree, bytesRead := deserializeVLQ(serialized[offset:])
+	offset += bytesRead
+	if offset >= len(serialized) {
+		return errDeserialize("unexpected end of data after tree")
 	}
 
-	return headerCode, numBitmapBytes, nil
+	// Deserialize the index.
+	idx, _ := deserializeVLQ(serialized[offset:])
+
+	// Populate the outpoint.
+	outpoint.Hash = hash
+	outpoint.Tree = int8(tree)
+	outpoint.Index = uint32(idx)
+
+	return nil
+}
+
+// recycleOutpointKey puts the provided byte slice, which should have been
+// obtained via the outpointKey function, back on the free list.
+func recycleOutpointKey(key *[]byte) {
+	outpointKeyPool.Put(key)
 }
 
 // serializeUtxoEntry returns the entry serialized to a format that is suitable
 // for long-term storage.  The format is described in detail above.
 func serializeUtxoEntry(entry *UtxoEntry) ([]byte, error) {
-	// Fully spent entries have no serialization.
-	if entry.IsFullySpent() {
+	// Spent entries have no serialization.
+	if entry.IsSpent() {
 		return nil, nil
 	}
 
-	// Determine the output order by sorting the sparse output index keys.
-	outputOrder := make([]int, 0, len(entry.sparseOutputs))
-	for outputIndex := range entry.sparseOutputs {
-		outputOrder = append(outputOrder, int(outputIndex))
-	}
-	sort.Ints(outputOrder)
-
-	// Encode the header code and determine the number of bytes the
-	// unspentness bitmap needs.
-	highIndex := uint32(outputOrder[len(outputOrder)-1])
-	headerCode, numBitmapBytes, err := utxoEntryHeaderCode(entry, highIndex)
-	if err != nil {
-		return nil, err
-	}
-
 	// Calculate the size needed to serialize the entry.
-	flags := encodeFlags(entry.isCoinBase, entry.hasExpiry, entry.txType, false)
-	size := serializeSizeVLQ(uint64(entry.txVersion)) +
-		serializeSizeVLQ(uint64(entry.height)) +
-		serializeSizeVLQ(uint64(entry.index)) +
+	const hasAmount = true
+	flags := encodeFlags(entry.IsCoinBase(), entry.HasExpiry(),
+		entry.TransactionType())
+	size := serializeSizeVLQ(uint64(entry.blockHeight)) +
+		serializeSizeVLQ(uint64(entry.blockIndex)) +
 		serializeSizeVLQ(uint64(flags)) +
-		serializeSizeVLQ(headerCode) + numBitmapBytes
-	for _, outputIndex := range outputOrder {
-		out := entry.sparseOutputs[uint32(outputIndex)]
-		if out.spent {
-			continue
-		}
-		size += compressedTxOutSize(uint64(out.amount), out.scriptVersion,
-			out.pkScript, currentCompressionVersion, out.compressed, true)
-	}
-	if entry.txType == stake.TxTypeSStx {
-		size += len(entry.stakeExtra)
+		compressedTxOutSize(uint64(entry.amount), entry.scriptVersion,
+			entry.pkScript, currentCompressionVersion, hasAmount)
+
+	if entry.ticketMinOuts != nil {
+		size += len(entry.ticketMinOuts.data)
 	}
 
-	// Serialize the version, block height, block index, and flags of the
-	// containing transaction, and "header code" which is a complex bitmap
-	// of spentness.
+	// Serialize the entry.
 	serialized := make([]byte, size)
-	offset := putVLQ(serialized, uint64(entry.txVersion))
-	offset += putVLQ(serialized[offset:], uint64(entry.height))
-	offset += putVLQ(serialized[offset:], uint64(entry.index))
+	offset := putVLQ(serialized, uint64(entry.blockHeight))
+	offset += putVLQ(serialized[offset:], uint64(entry.blockIndex))
 	offset += putVLQ(serialized[offset:], uint64(flags))
-	offset += putVLQ(serialized[offset:], headerCode)
-
-	// Serialize the unspentness bitmap.
-	for i := uint32(0); i < uint32(numBitmapBytes); i++ {
-		unspentBits := byte(0)
-		for j := uint32(0); j < 8; j++ {
-			// The first 2 outputs are encoded via the header code,
-			// so adjust the output index accordingly.
-			if !entry.IsOutputSpent(2 + i*8 + j) {
-				unspentBits |= 1 << uint8(j)
-			}
-		}
-		serialized[offset] = unspentBits
-		offset++
-	}
-
-	// Serialize the compressed unspent transaction outputs.  Outputs that
-	// are already compressed are serialized without modifications.
-	for _, outputIndex := range outputOrder {
-		out := entry.sparseOutputs[uint32(outputIndex)]
-		if out.spent {
-			continue
-		}
-
-		offset += putCompressedTxOut(serialized[offset:],
-			uint64(out.amount), out.scriptVersion, out.pkScript,
-			currentCompressionVersion, out.compressed, true)
-	}
+	offset += putCompressedTxOut(serialized[offset:], uint64(entry.amount),
+		entry.scriptVersion, entry.pkScript, currentCompressionVersion, hasAmount)
 
-	if entry.txType == stake.TxTypeSStx {
-		copy(serialized[offset:], entry.stakeExtra)
+	if entry.ticketMinOuts != nil {
+		copy(serialized[offset:], entry.ticketMinOuts.data)
 	}
 
 	return serialized, nil
@@ -1033,17 +961,10 @@ func serializeUtxoEntry(entry *UtxoEntry) ([]byte, error) {
 // deserializeUtxoEntry decodes a utxo entry from the passed serialized byte
 // slice into a new UtxoEntry using a format that is suitable for long-term
 // storage.  The format is described in detail above.
-func deserializeUtxoEntry(serialized []byte) (*UtxoEntry, error) {
-	// Deserialize the version.
-	version, bytesRead := deserializeVLQ(serialized)
-	offset := bytesRead
-	if offset >= len(serialized) {
-		return nil, errDeserialize("unexpected end of data after version")
-	}
-
+func deserializeUtxoEntry(serialized []byte, txOutIndex uint32) (*UtxoEntry, error) {
 	// Deserialize the block height.
-	blockHeight, bytesRead := deserializeVLQ(serialized[offset:])
-	offset += bytesRead
+	blockHeight, bytesRead := deserializeVLQ(serialized)
+	offset := bytesRead
 	if offset >= len(serialized) {
 		return nil, errDeserialize("unexpected end of data after height")
 	}
@@ -1061,133 +982,76 @@ func deserializeUtxoEntry(serialized []byte) (*UtxoEntry, error) {
 	if offset >= len(serialized) {
 		return nil, errDeserialize("unexpected end of data after flags")
 	}
-	isCoinBase, hasExpiry, txType, _ := decodeFlags(byte(flags))
+	isCoinBase, hasExpiry, txType := decodeFlags(txOutFlags(flags))
 
-	// Deserialize the header code.
-	code, bytesRead := deserializeVLQ(serialized[offset:])
-	offset += bytesRead
-	if offset >= len(serialized) {
-		return nil, errDeserialize("unexpected end of data after header")
+	// Decode the compressed unspent transaction output.
+	amount, scriptVersion, script, bytesRead, err :=
+		decodeCompressedTxOut(serialized[offset:], currentCompressionVersion,
+			true)
+	if err != nil {
+		return nil, errDeserialize(fmt.Sprintf("unable to decode utxo: %v", err))
 	}
+	offset += bytesRead
 
-	// Decode the header code.
-	//
-	// Bit 0 indicates output 0 is unspent.
-	// Bit 1 indicates output 1 is unspent.
-	// Bits 2-x encodes the number of non-zero unspentness bitmap bytes that
-	// follow.  When both output 0 and 1 are spent, it encodes N-1.
-	output0Unspent := code&0x01 != 0
-	output1Unspent := code&0x02 != 0
-	numBitmapBytes := code >> 2
-	if !output0Unspent && !output1Unspent {
-		numBitmapBytes++
-	}
-
-	// Ensure there are enough bytes left to deserialize the unspentness
-	// bitmap.
-	if uint64(len(serialized[offset:])) < numBitmapBytes {
-		return nil, errDeserialize("unexpected end of data for " +
-			"unspentness bitmap")
-	}
-
-	// Create a new utxo entry with the details deserialized above to house
-	// all of the utxos.
-	entry := newUtxoEntry(uint16(version), uint32(blockHeight),
-		uint32(blockIndex), isCoinBase, hasExpiry, txType)
-
-	// Add sparse output for unspent outputs 0 and 1 as needed based on the
-	// details provided by the header code.
-	var outputIndexes []uint32
-	if output0Unspent {
-		outputIndexes = append(outputIndexes, 0)
-	}
-	if output1Unspent {
-		outputIndexes = append(outputIndexes, 1)
-	}
-
-	// Decode the unspentness bitmap adding a sparse output for each unspent
-	// output.
-	for i := uint32(0); i < uint32(numBitmapBytes); i++ {
-		unspentBits := serialized[offset]
-		for j := uint32(0); j < 8; j++ {
-			if unspentBits&0x01 != 0 {
-				// The first 2 outputs are encoded via the
-				// header code, so adjust the output number
-				// accordingly.
-				outputNum := 2 + i*8 + j
-				outputIndexes = append(outputIndexes, outputNum)
-			}
-			unspentBits >>= 1
-		}
-		offset++
-	}
-
-	// Decode and add all of the utxos.
-	for i, outputIndex := range outputIndexes {
-		// Decode the next utxo.  The script and amount fields of the
-		// utxo output are left compressed so decompression can be
-		// avoided on those that are not accessed.  This is done since
-		// it is quite common for a redeeming transaction to only
-		// reference a single utxo from a referenced transaction.
-		//
-		// 'true' below instructs the method to deserialize a stored
-		// amount.
-		amount, scriptVersion, compScript, bytesRead, err :=
-			decodeCompressedTxOut(serialized[offset:], currentCompressionVersion,
-				true)
+	// Create a new utxo entry with the details deserialized above.
+	const spent = false
+	const modified = false
+	entry := &UtxoEntry{
+		amount:        amount,
+		pkScript:      script,
+		blockHeight:   uint32(blockHeight),
+		blockIndex:    uint32(blockIndex),
+		scriptVersion: scriptVersion,
+		packedFlags: encodeUtxoFlags(isCoinBase, spent, modified, hasExpiry,
+			txType),
+	}
+
+	// Copy the minimal outputs if this was a ticket submission output.
+	if isTicketSubmissionOutput(txType, txOutIndex) {
+		sz, err := readDeserializeSizeOfMinimalOutputs(serialized[offset:])
 		if err != nil {
-			return nil, errDeserialize(fmt.Sprintf("unable to "+
-				"decode utxo at index %d: %v", i, err))
+			return nil, errDeserialize(fmt.Sprintf("unable to decode "+
+				"ticket outputs: %v", err))
 		}
-		offset += bytesRead
-
-		entry.sparseOutputs[outputIndex] = &utxoOutput{
-			spent:         false,
-			compressed:    true,
-			scriptVersion: scriptVersion,
-			pkScript:      compScript,
-			amount:        amount,
+		entry.ticketMinOuts = &ticketMinimalOutputs{
+			data: make([]byte, sz),
 		}
-	}
-
-	// Copy the stake extra data if this was a ticket.
-	if entry.txType == stake.TxTypeSStx {
-		stakeExtra := make([]byte, len(serialized[offset:]))
-		copy(stakeExtra, serialized[offset:])
-		entry.stakeExtra = stakeExtra
+		copy(entry.ticketMinOuts.data, serialized[offset:offset+sz])
 	}
 
 	return entry, nil
 }
 
-// dbFetchUtxoEntry uses an existing database transaction to fetch all unspent
-// outputs for the provided Bitcoin transaction hash from the utxo set.
+// dbFetchUtxoEntry uses an existing database transaction to fetch the specified
+// transaction output from the utxo set.
 //
-// When there is no entry for the provided hash, nil will be returned for the
-// both the entry and the error.
-func dbFetchUtxoEntry(dbTx database.Tx, hash *chainhash.Hash) (*UtxoEntry, error) {
-	// Fetch the unspent transaction output information for the passed
-	// transaction hash.  Return now when there is no entry.
+// When there is no entry for the provided output, nil will be returned for both
+// the entry and the error.
+func dbFetchUtxoEntry(dbTx database.Tx, outpoint wire.OutPoint) (*UtxoEntry, error) {
+	// Fetch the unspent transaction output information for the passed transaction
+	// output.  Return now when there is no entry.
+	key := outpointKey(outpoint)
 	utxoBucket := dbTx.Metadata().Bucket(utxoSetBucketName)
-	serializedUtxo := utxoBucket.Get(hash[:])
+	serializedUtxo := utxoBucket.Get(*key)
+	recycleOutpointKey(key)
 	if serializedUtxo == nil {
 		return nil, nil
 	}
 
-	// A non-nil zero-length entry means there is an entry in the database
-	// for a fully spent transaction which should never be the case.
+	// A non-nil zero-length entry means there is an entry in the database for a
+	// spent transaction output which should never be the case.
 	if len(serializedUtxo) == 0 {
-		return nil, AssertError(fmt.Sprintf("database contains entry "+
-			"for fully spent tx %v", hash))
+		return nil, AssertError(fmt.Sprintf("database contains entry for spent tx "+
+			"output %v", outpoint))
 	}
 
 	// Deserialize the utxo entry and return it.
-	entry, err := deserializeUtxoEntry(serializedUtxo)
+	entry, err := deserializeUtxoEntry(serializedUtxo, outpoint.Index)
 	if err != nil {
-		// Ensure any deserialization errors are returned as database
-		// corruption errors.
+		// Ensure any deserialization errors are returned as database corruption
+		// errors.
 		if isDeserializeErr(err) {
-			str := fmt.Sprintf("corrupt utxo entry for %v: %v", hash, err)
+			str := fmt.Sprintf("corrupt utxo entry for %v: %v", outpoint, err)
 			return nil, makeDbErr(database.ErrCorruption, str)
 		}
 
@@ -1197,51 +1061,54 @@ func dbFetchUtxoEntry(dbTx database.Tx, hash *chainhash.Hash) (*UtxoEntry, error
 	return entry, nil
 }
 
-// dbFetchUxtoStats fetches statistics on the current unspent trnsaction output set.
+// dbFetchUxtoStats fetches statistics on the current unspent transaction output
+// set.
 func dbFetchUtxoStats(dbTx database.Tx) (*UtxoStats, error) {
 	utxoBucket := dbTx.Metadata().Bucket(utxoSetBucketName)
 
 	var stats UtxoStats
-	transactions := make(map[chainhash.Hash]int64)
+	transactions := make(map[chainhash.Hash]struct{})
 	leaves := make([]chainhash.Hash, 0)
 	cursor := utxoBucket.Cursor()
 
 	for ok := cursor.First(); ok; ok = cursor.Next() {
 		key := cursor.Key()
-		hash := chainhash.HashH(key)
+		var outpoint wire.OutPoint
+		err := decodeOutpointKey(key, &outpoint)
+		if err != nil {
+			return nil, err
+		}
+
 		serializedUtxo := cursor.Value()
 		entrySize := len(serializedUtxo)
 
-		// A non-nil zero-length entry means there is an entry in the database
-		// for a fully spent transaction which should never be the case.
+		// A non-nil zero-length entry means there is an entry in the database for a
+		// spent transaction output which should never be the case.
 		if entrySize == 0 {
-			return nil, AssertError(fmt.Sprintf("database contains entry "+
-				"for fully spent tx %s", hash))
+			return nil, AssertError(fmt.Sprintf("database contains entry for spent "+
+				"tx output %v", outpoint))
 		}
 
 		stats.Utxos++
 		stats.Size += int64(entrySize)
-		transactions[hash]++
+		transactions[outpoint.Hash] = struct{}{}
 
 		leaves = append(leaves, chainhash.HashH(serializedUtxo))
 
-		// Deserialize the utxo entry and return it.
-		entry, err := deserializeUtxoEntry(serializedUtxo)
+		// Deserialize the utxo entry.
+		entry, err := deserializeUtxoEntry(serializedUtxo, outpoint.Index)
 		if err != nil {
-			// Ensure any deserialization errors are returned as database
-			// corruption errors.
+			// Ensure any deserialization errors are returned as database corruption
+			// errors.
 			if isDeserializeErr(err) {
-				str := fmt.Sprintf("corrupt utxo entry for %v: %v", hash, err)
+				str := fmt.Sprintf("corrupt utxo entry for %v: %v", outpoint, err)
 				return nil, makeDbErr(database.ErrCorruption, str)
 			}
 
 			return nil, err
 		}
 
-		for _, out := range entry.sparseOutputs {
-			stats.Utxos++
-			stats.Total += out.amount
-		}
+		stats.Total += entry.amount
 	}
 
 	stats.SerializedHash = standalone.CalcMerkleRootInPlace(leaves)
@@ -1250,42 +1117,40 @@ func dbFetchUtxoStats(dbTx database.Tx) (*UtxoStats, error) {
 	return &stats, nil
 }
 
-// dbPutUtxoView uses an existing database transaction to update the utxo set
-// in the database based on the provided utxo view contents and state.  In
-// particular, only the entries that have been marked as modified are written
-// to the database.
+// dbPutUtxoView uses an existing database transaction to update the utxo set in
+// the database based on the provided utxo view contents and state.  In
+// particular, only the entries that have been marked as modified are written to
+// the database.
 func dbPutUtxoView(dbTx database.Tx, view *UtxoViewpoint) error {
 	utxoBucket := dbTx.Metadata().Bucket(utxoSetBucketName)
-	for txHashIter, entry := range view.entries {
+	for outpoint, entry := range view.entries {
 		// No need to update the database if the entry was not modified.
-		if entry == nil || !entry.modified {
+		if entry == nil || !entry.isModified() {
 			continue
 		}
 
-		// Serialize the utxo entry without any entries that have been
-		// spent.
-		serialized, err := serializeUtxoEntry(entry)
-		if err != nil {
-			return err
-		}
-
-		// Make a copy of the hash because the iterator changes on each
-		// loop iteration and thus slicing it directly would cause the
-		// data to change out from under the put/delete funcs below.
-		txHash := txHashIter
-
-		// Remove the utxo entry if it is now fully spent.
-		if serialized == nil {
-			if err := utxoBucket.Delete(txHash[:]); err != nil {
+		// Remove the utxo entry if it is spent.
+		if entry.IsSpent() {
+			key := outpointKey(outpoint)
+			err := utxoBucket.Delete(*key)
+			recycleOutpointKey(key)
+			if err != nil {
 				return err
 			}
 
 			continue
 		}
 
-		// At this point the utxo entry is not fully spent, so store its
-		// serialization in the database.
-		err = utxoBucket.Put(txHash[:], serialized)
+		// Serialize and store the utxo entry.
+		serialized, err := serializeUtxoEntry(entry)
+		if err != nil {
+			return err
+		}
+		key := outpointKey(outpoint)
+		err = utxoBucket.Put(*key, serialized)
+		// NOTE: The key is intentionally not recycled here since the database
+		// interface contract prohibits modifications.  It will be garbage collected
+		// normally when the database is done with it.
 		if err != nil {
 			return err
 		}
@@ -1959,7 +1824,12 @@ func (b *BlockChain) initChainState(ctx context.Context) error {
 
 		return nil
 	})
-	return err
+	if err != nil {
+		return err
+	}
+
+	// Upgrade the database post block index load as needed.
+	return upgradeDBPostBlockIndexLoad(ctx, b)
 }
 
 // dbFetchBlockByNode uses an existing database transaction to retrieve the raw
diff --git a/blockchain/compress.go b/blockchain/compress.go
index d6765a66..995e0a61 100644
--- a/blockchain/compress.go
+++ b/blockchain/compress.go
@@ -579,60 +579,35 @@ func decompressTxOutAmount(amount uint64) uint64 {
 // -----------------------------------------------------------------------------
 
 // compressedTxOutSize returns the number of bytes the passed transaction output
-// fields would take when encoded with the format described above.  The
-// preCompressed flag indicates the provided amount and script are already
-// compressed.  This is useful since loaded utxo entries are not decompressed
-// until the output is accessed.
+// fields would take when encoded with the format described above.
 func compressedTxOutSize(amount uint64, scriptVersion uint16, pkScript []byte,
-	compressionVersion uint32, preCompressed bool, hasAmount bool) int {
+	compressionVersion uint32, hasAmount bool) int {
+
 	scriptVersionSize := serializeSizeVLQ(uint64(scriptVersion))
-	if preCompressed && !hasAmount {
-		return scriptVersionSize + len(pkScript)
-	}
-	if preCompressed && hasAmount {
-		return scriptVersionSize + serializeSizeVLQ(compressTxOutAmount(amount)) +
-			len(pkScript)
-	}
-	if !preCompressed && !hasAmount {
+	if !hasAmount {
 		return scriptVersionSize + compressedScriptSize(scriptVersion,
 			pkScript, compressionVersion)
 	}
 
-	// if !preCompressed && hasAmount
 	return scriptVersionSize + serializeSizeVLQ(compressTxOutAmount(amount)) +
 		compressedScriptSize(scriptVersion, pkScript, compressionVersion)
 }
 
-// putCompressedTxOut potentially compresses the passed amount and script
-// according to their domain specific compression algorithms and encodes them
-// directly into the passed target byte slice with the format described above.
-// The preCompressed flag indicates the provided amount and script are already
-// compressed in which case the values are not modified.  This is useful since
-// loaded utxo entries are not decompressed until the output is accessed.  The
-// target byte slice must be at least large enough to handle the number of bytes
-// returned by the compressedTxOutSize function or it will panic.
+// putCompressedTxOut compresses the passed amount and script according to their
+// domain specific compression algorithms and encodes them directly into the
+// passed target byte slice with the format described above.  The target byte
+// slice must be at least large enough to handle the number of bytes returned by
+// the compressedTxOutSize function or it will panic.
 func putCompressedTxOut(target []byte, amount uint64, scriptVersion uint16,
-	pkScript []byte, compressionVersion uint32, preCompressed bool,
-	hasAmount bool) int {
-	if preCompressed && hasAmount {
-		offset := putVLQ(target, compressTxOutAmount(amount))
-		offset += putVLQ(target[offset:], uint64(scriptVersion))
-		copy(target[offset:], pkScript)
-		return offset + len(pkScript)
-	}
-	if preCompressed && !hasAmount {
-		offset := putVLQ(target, uint64(scriptVersion))
-		copy(target[offset:], pkScript)
-		return offset + len(pkScript)
-	}
-	if !preCompressed && !hasAmount {
+	pkScript []byte, compressionVersion uint32, hasAmount bool) int {
+
+	if !hasAmount {
 		offset := putVLQ(target, uint64(scriptVersion))
 		offset += putCompressedScript(target[offset:], scriptVersion, pkScript,
 			compressionVersion)
 		return offset
 	}
 
-	// if !preCompressed && hasAmount
 	offset := putVLQ(target, compressTxOutAmount(amount))
 	offset += putVLQ(target[offset:], uint64(scriptVersion))
 	offset += putCompressedScript(target[offset:], scriptVersion, pkScript,
@@ -641,10 +616,11 @@ func putCompressedTxOut(target []byte, amount uint64, scriptVersion uint16,
 }
 
 // decodeCompressedTxOut decodes the passed compressed txout, possibly followed
-// by other data, into its compressed amount and compressed script and returns
-// them along with the number of bytes they occupied.
+// by other data, into its uncompressed amount and script and returns them along
+// with the number of bytes they occupied prior to decompression.
 func decodeCompressedTxOut(serialized []byte, compressionVersion uint32,
 	hasAmount bool) (int64, uint16, []byte, int, error) {
+
 	var amount int64
 	var bytesRead int
 	var offset int
@@ -684,66 +660,50 @@ func decodeCompressedTxOut(serialized []byte, compressionVersion uint32,
 			scriptSize))
 	}
 
-	// Make a copy of the compressed script so the original serialized data
-	// can be released as soon as possible.
-	compressedScript := make([]byte, scriptSize)
-	copy(compressedScript, serialized[offset:offset+scriptSize])
+	// Decompress the script.
+	script := decompressScript(serialized[offset:offset+scriptSize],
+		compressionVersion)
 
-	return amount, uint16(scriptVersion), compressedScript,
-		offset + scriptSize, nil
+	return amount, uint16(scriptVersion), script, offset + scriptSize, nil
 }
 
-// -----------------------------------------------------------------------------
-// Decred specific transaction encoding flags
-//
-// Details about a transaction needed to determine how it may be spent
-// according to consensus rules are given by these flags.
+// txOutFlags defines additional information and state for transaction outputs.
+// This is used when serializing both unspent and spent transaction outputs.
 //
-// The following details are encoded into a single byte, where the index
-// of the bit is given in zeroeth order:
-//     0: Is coinbase
-//     1: Has an expiry
-//   2-4: Transaction type
-//     5: Unused
-//     6: Fully spent
-//     7: Unused
-//
-// 0, 1, and 6 are bit flags, while the transaction type is encoded with a bitmask
-// and used to describe the underlying int.
-//
-// The fully spent flag should always come as the *last* flag (highest bit index)
-// in this data type should flags be updated to include more rules in the future,
-// such as rules governing new script OP codes. This ensures that we may still use
-// these flags in the UTX serialized data without consequence, where the last
-// flag indicating fully spent will always be zeroed. Note that currently the
-// fully spent flag is stored in bit 6 so that when serializing the flags as a
-// VLQ integer it still fits into a single byte.
-//
-// -----------------------------------------------------------------------------
+// The bit representation is:
+//   bit  0     - containing transaction is a coinbase
+//   bit  1     - containing transaction has an expiry
+//   bits 2-5   - transaction type
+//   bits 6-7   - unused
+type txOutFlags uint8
 
 const (
-	// txTypeBitmask describes the bitmask that yields the 3rd, 4th and 5th
-	// bits from the flags byte.
-	txTypeBitmask = 0x1c
+	// txOutFlagCoinBase indicates that a txout was contained in a coinbase tx.
+	txOutFlagCoinBase = 1 << 0
+
+	// txOutFlagHasExpiry indicates that a txout was contained in a tx that included
+	// an expiry.
+	txOutFlagHasExpiry = 1 << 1
 
-	// txTypeShift is the number of bits to shift flags to the right to yield the
-	// correct integer value after applying the bitmask with AND.
-	txTypeShift = 2
+	// txOutFlagTxTypeBitmask describes the bitmask that yields bits 2-5 from
+	// txoFlags.
+	txOutFlagTxTypeBitmask = 0x3c
+
+	// txOutFlagTxTypeShift is the number of bits to shift txoFlags to the right
+	// to yield the correct integer value after applying the bitmask with AND.
+	txOutFlagTxTypeShift = 2
 )
 
 // encodeFlags encodes transaction flags into a single byte.
-func encodeFlags(isCoinBase bool, hasExpiry bool, txType stake.TxType, fullySpent bool) byte {
-	b := uint8(txType)
-	b <<= txTypeShift
+func encodeFlags(isCoinBase bool, hasExpiry bool, txType stake.TxType) txOutFlags {
+	b := txOutFlags(txType)
+	b <<= txOutFlagTxTypeShift
 
 	if isCoinBase {
-		b |= 0x01 // Set bit 0
+		b |= txOutFlagCoinBase
 	}
 	if hasExpiry {
-		b |= 0x02 // Set bit 1
-	}
-	if fullySpent {
-		b |= 0x40 // Set bit 6
+		b |= txOutFlagHasExpiry
 	}
 
 	return b
@@ -751,18 +711,12 @@ func encodeFlags(isCoinBase bool, hasExpiry bool, txType stake.TxType, fullySpen
 
 // decodeFlags decodes transaction flags from a single byte into their respective
 // data types.
-func decodeFlags(b byte) (bool, bool, stake.TxType, bool) {
-	isCoinBase := b&0x01 != 0
-	hasExpiry := b&(1<<1) != 0
-	fullySpent := b&(1<<6) != 0
-	txType := stake.TxType((b & txTypeBitmask) >> txTypeShift)
-
-	return isCoinBase, hasExpiry, txType, fullySpent
-}
+func decodeFlags(flags txOutFlags) (bool, bool, stake.TxType) {
+	isCoinBase := flags&txOutFlagCoinBase == txOutFlagCoinBase
+	hasExpiry := flags&txOutFlagHasExpiry == txOutFlagHasExpiry
+	txType := (flags & txOutFlagTxTypeBitmask) >> txOutFlagTxTypeShift
 
-// decodeFlagsFullySpent decodes whether or not a transaction was fully spent.
-func decodeFlagsFullySpent(b byte) bool {
-	return b&(1<<6) != 0
+	return isCoinBase, hasExpiry, stake.TxType(txType)
 }
 
 // absInt64 computes the absolute value of the given int64 and converts it into
diff --git a/blockchain/error.go b/blockchain/error.go
index 6420f423..1c62fd45 100644
--- a/blockchain/error.go
+++ b/blockchain/error.go
@@ -392,11 +392,6 @@ const (
 	// more than it is allowed.
 	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 = ErrorKind("ErrDiscordantTxTree")
-
 	// ErrStakeFees indicates an error with the fees found in the stake
 	// transaction tree.
 	ErrStakeFees = ErrorKind("ErrStakeFees")
diff --git a/blockchain/scriptval.go b/blockchain/scriptval.go
index 7647f1b8..cf867728 100644
--- a/blockchain/scriptval.go
+++ b/blockchain/scriptval.go
@@ -56,48 +56,34 @@ out:
 			break out
 
 		case txVI := <-v.validateChan:
-			// Ensure the referenced input transaction is available.
+			// Ensure the referenced input utxo is available.
 			txIn := txVI.txIn
-			originTxHash := &txIn.PreviousOutPoint.Hash
-			originTxIndex := txIn.PreviousOutPoint.Index
-			txEntry := v.utxoView.LookupEntry(originTxHash)
-			if txEntry == nil {
-				str := fmt.Sprintf("unable to find input "+
-					"transaction %v referenced from "+
-					"transaction %v", originTxHash,
-					txVI.tx.Hash())
-				err := ruleError(ErrMissingTxOut, str)
-				v.sendResult(ctx, err)
-				break out
-			}
-
-			// Ensure the referenced input transaction public key
-			// script is available.
-			pkScript := txEntry.PkScriptByIndex(originTxIndex)
-			if pkScript == nil {
+			utxo := v.utxoView.LookupEntry(txIn.PreviousOutPoint)
+			if utxo == nil {
 				str := fmt.Sprintf("unable to find unspent "+
-					"output %v script referenced from "+
+					"output %v referenced from "+
 					"transaction %s:%d",
 					txIn.PreviousOutPoint, txVI.tx.Hash(),
 					txVI.txInIndex)
-				err := ruleError(ErrBadTxInput, str)
+				err := ruleError(ErrMissingTxOut, str)
 				v.sendResult(ctx, err)
 				break out
 			}
 
 			// Create a new script engine for the script pair.
 			sigScript := txIn.SignatureScript
-			version := txEntry.ScriptVersionByIndex(originTxIndex)
+			version := utxo.ScriptVersion()
+			pkScript := utxo.PkScript()
 
 			vm, err := txscript.NewEngine(pkScript, txVI.tx.MsgTx(),
 				txVI.txInIndex, v.flags, version, v.sigCache)
 			if err != nil {
 				str := fmt.Sprintf("failed to parse input "+
-					"%s:%d which references output %s:%d - "+
+					"%s:%d which references output %v - "+
 					"%v (input script bytes %x, prev output "+
 					"script bytes %x)", txVI.tx.Hash(),
-					txVI.txInIndex, originTxHash,
-					originTxIndex, err, sigScript, pkScript)
+					txVI.txInIndex, txIn.PreviousOutPoint,
+					err, sigScript, pkScript)
 				err := ruleError(ErrScriptMalformed, str)
 				v.sendResult(ctx, err)
 				break out
@@ -106,11 +92,11 @@ out:
 			// Execute the script pair.
 			if err := vm.Execute(); err != nil {
 				str := fmt.Sprintf("failed to validate input "+
-					"%s:%d which references output %s:%d - "+
+					"%s:%d which references output %v - "+
 					"%v (input script bytes %x, prev output "+
 					"script bytes %x)", txVI.tx.Hash(),
-					txVI.txInIndex, originTxHash,
-					originTxIndex, err, sigScript, pkScript)
+					txVI.txInIndex, txIn.PreviousOutPoint,
+					err, sigScript, pkScript)
 				err := ruleError(ErrScriptValidation, str)
 				v.sendResult(ctx, err)
 				break out
diff --git a/blockchain/sequencelock.go b/blockchain/sequencelock.go
index 622a86be..83417743 100644
--- a/blockchain/sequencelock.go
+++ b/blockchain/sequencelock.go
@@ -69,7 +69,7 @@ func (b *BlockChain) calcSequenceLock(node *blockNode, tx *dcrutil.Tx, view *Utx
 			continue
 		}
 
-		utxo := view.LookupEntry(&txIn.PreviousOutPoint.Hash)
+		utxo := view.LookupEntry(txIn.PreviousOutPoint)
 		if utxo == nil {
 			str := fmt.Sprintf("output %v referenced from "+
 				"transaction %s:%d either does not exist or "+
diff --git a/blockchain/stakeext.go b/blockchain/stakeext.go
index e486339b..244ba694 100644
--- a/blockchain/stakeext.go
+++ b/blockchain/stakeext.go
@@ -10,6 +10,7 @@ import (
 	"github.com/decred/dcrd/database/v2"
 	"github.com/decred/dcrd/dcrutil/v4"
 	"github.com/decred/dcrd/txscript/v4"
+	"github.com/decred/dcrd/wire"
 )
 
 // NextLotteryData returns the next tickets eligible for spending as SSGen
@@ -110,15 +111,14 @@ func (b *BlockChain) TicketsWithAddress(address dcrutil.Address, isTreasuryEnabl
 	var ticketsWithAddr []chainhash.Hash
 	err := b.db.View(func(dbTx database.Tx) error {
 		for _, hash := range tickets {
-			utxo, err := dbFetchUtxoEntry(dbTx, &hash)
+			outpoint := wire.OutPoint{Hash: hash, Index: 0, Tree: wire.TxTreeStake}
+			utxo, err := dbFetchUtxoEntry(dbTx, outpoint)
 			if err != nil {
 				return err
 			}
 
-			_, addrs, _, err :=
-				txscript.ExtractPkScriptAddrs(utxo.ScriptVersionByIndex(0),
-					utxo.PkScriptByIndex(0), b.chainParams,
-					isTreasuryEnabled)
+			_, addrs, _, err := txscript.ExtractPkScriptAddrs(utxo.ScriptVersion(),
+				utxo.PkScript(), b.chainParams, isTreasuryEnabled)
 			if err != nil {
 				return err
 			}
@@ -223,12 +223,13 @@ func (b *BlockChain) TicketPoolValue() (dcrutil.Amount, error) {
 	var amt int64
 	err := b.db.View(func(dbTx database.Tx) error {
 		for _, hash := range sn.LiveTickets() {
-			utxo, err := dbFetchUtxoEntry(dbTx, &hash)
+			outpoint := wire.OutPoint{Hash: hash, Index: 0, Tree: wire.TxTreeStake}
+			utxo, err := dbFetchUtxoEntry(dbTx, outpoint)
 			if err != nil {
 				return err
 			}
 
-			amt += utxo.sparseOutputs[0].amount
+			amt += utxo.Amount()
 		}
 		return nil
 	})
diff --git a/blockchain/upgrade.go b/blockchain/upgrade.go
index 5bd38cc4..730a1d49 100644
--- a/blockchain/upgrade.go
+++ b/blockchain/upgrade.go
@@ -496,7 +496,7 @@ type scriptSource map[wire.OutPoint]scriptSourceEntry
 // PrevScript returns the script and script version associated with the provided
 // previous outpoint along with a bool that indicates whether or not the
 // requested entry exists.  This ensures the caller is able to distinguish
-// between missing entry and empty v0 scripts.
+// between missing entries and empty v0 scripts.
 func (s scriptSource) PrevScript(prevOut *wire.OutPoint) (uint16, []byte, bool) {
 	entry, ok := s[*prevOut]
 	if !ok {
@@ -1789,6 +1789,1184 @@ func upgradeToVersion8(ctx context.Context, db database.DB, dbInfo *databaseInfo
 	return nil
 }
 
+// migrateUtxoSetVersion2To3 migrates all utxoset entries from the v2 bucket to
+// a v3 bucket and removes the old v2 bucket.
+//
+// The utxoset is guaranteed to be fully updated if this returns without
+// failure.
+func migrateUtxoSetVersion2To3(ctx context.Context, db database.DB) error {
+	// Hardcoded bucket and key names so updates do not affect old upgrades.
+	v2BucketName := []byte("utxosetv2")
+	v3BucketName := []byte("utxosetv3")
+
+	log.Info("Migrating database utxoset.  This may take a while...")
+	start := time.Now()
+
+	// Create the new utxoset bucket as needed.
+	err := db.Update(func(dbTx database.Tx) error {
+		_, err := dbTx.Metadata().CreateBucketIfNotExists(v3BucketName)
+		return err
+	})
+	if err != nil {
+		return err
+	}
+
+	// doBatch contains the primary logic for upgrading the utxoset from version
+	// 2 to 3 in batches.  This is done because attempting to migrate in a
+	// single database transaction could result in massive memory usage and
+	// could potentially crash on many systems due to ulimits.
+	//
+	// It returns whether or not all entries have been updated.
+	const maxEntries = 20000
+	var resumeOffset uint32
+	var totalMigrated uint64
+	doBatch := func(dbTx database.Tx) (bool, error) {
+		meta := dbTx.Metadata()
+		v2Bucket := meta.Bucket(v2BucketName)
+		if v2Bucket == nil {
+			return false, fmt.Errorf("bucket %s does not exist", v2BucketName)
+		}
+
+		v3Bucket := meta.Bucket(v3BucketName)
+		if v3Bucket == nil {
+			return false, fmt.Errorf("bucket %s does not exist", v3BucketName)
+		}
+
+		// Migrate utxoset entries so long as the max number of entries for this
+		// batch has not been exceeded.
+		var logProgress bool
+		var numMigrated, numIterated uint32
+		err := v2Bucket.ForEach(func(oldKey, oldSerialized []byte) error {
+			if interruptRequested(ctx) {
+				logProgress = true
+				return errInterruptRequested
+			}
+
+			if numMigrated >= maxEntries {
+				logProgress = true
+				return errBatchFinished
+			}
+
+			// Skip entries that have already been migrated in previous batches.
+			numIterated++
+			if numIterated-1 < resumeOffset {
+				return nil
+			}
+			resumeOffset++
+
+			// Old key was the transaction hash.
+			var txHash chainhash.Hash
+			copy(txHash[:], oldKey)
+
+			// Deserialize the legacy V2 entry which included all utxos for the given
+			// transaction.
+			//
+			// The legacy V2 format is as follows:
+			//
+			//   <version><height><header code><unspentness bitmap>
+			//	 [<compressed txouts>,...]
+			//
+			//   Field                 Type     Size
+			//   transaction version   VLQ      variable
+			//   block height          VLQ      variable
+			//   block index           VLQ      variable
+			//   flags                 VLQ      variable (currently 1 byte)
+			//   header code           VLQ      variable
+			//   unspentness bitmap    []byte   variable
+			//   compressed txouts
+			//     compressed amount   VLQ      variable
+			//     script version      VLQ      variable
+			//     compressed script   []byte   variable
+			//   stakeExtra            []byte   variable
+			//
+			// The serialized flags code format is:
+			//   bit  0   - containing transaction is a coinbase
+			//   bit  1   - containing transaction has an expiry
+			//   bits 2-4 - transaction type
+			//   bit  5   - unused
+			//   bit  6   - is fully spent
+			//   bit  7   - unused
+			//
+			// The serialized header code format is:
+			//   bit 0 - output zero is unspent
+			//   bit 1 - output one is unspent
+			//   bits 2-x - number of bytes in unspentness bitmap.  When both bits 1
+			//     and 2 are unset, it encodes N-1 since there must be at least one
+			//     unspent output.
+			//
+			// The stake extra field contains minimally encoded outputs for all
+			// consensus-related outputs in the stake transaction. It is only
+			// encoded for tickets.
+
+			// Deserialize the version.
+			//
+			// NOTE: Ignore version since it is no longer used in the new format.
+			_, bytesRead := deserializeVLQ(oldSerialized)
+			offset := bytesRead
+			if offset >= len(oldSerialized) {
+				return errDeserialize("unexpected end of data after version")
+			}
+
+			// Deserialize the block height.
+			blockHeight, bytesRead := deserializeVLQ(oldSerialized[offset:])
+			offset += bytesRead
+			if offset >= len(oldSerialized) {
+				return errDeserialize("unexpected end of data after height")
+			}
+
+			// Deserialize the block index.
+			blockIndex, bytesRead := deserializeVLQ(oldSerialized[offset:])
+			offset += bytesRead
+			if offset >= len(oldSerialized) {
+				return errDeserialize("unexpected end of data after index")
+			}
+
+			// Deserialize the flags.
+			flags, bytesRead := deserializeVLQ(oldSerialized[offset:])
+			offset += bytesRead
+			if offset >= len(oldSerialized) {
+				return errDeserialize("unexpected end of data after flags")
+			}
+			// Decode the flags.  The format is:
+			//     0: Is coinbase
+			//     1: Has an expiry
+			//   2-4: Transaction type
+			//     5: Unused
+			//     6: Fully spent
+			//     7: Unused
+			isCoinBase := flags&0x01 != 0
+			hasExpiry := flags&(1<<1) != 0
+			txType := stake.TxType((flags & 0x1c) >> 2)
+
+			// Deserialize the header code.
+			code, bytesRead := deserializeVLQ(oldSerialized[offset:])
+			offset += bytesRead
+			if offset >= len(oldSerialized) {
+				return errDeserialize("unexpected end of data after header")
+			}
+
+			// Decode the header code.
+			//
+			// Bit 0 indicates output 0 is unspent.
+			// Bit 1 indicates output 1 is unspent.
+			// Bits 2-x encodes the number of non-zero unspentness bitmap bytes that
+			// follow.  When both output 0 and 1 are spent, it encodes N-1.
+			output0Unspent := code&0x01 != 0
+			output1Unspent := code&0x02 != 0
+			numBitmapBytes := code >> 2
+			if !output0Unspent && !output1Unspent {
+				numBitmapBytes++
+			}
+
+			// Ensure there are enough bytes left to deserialize the unspentness
+			// bitmap.
+			if uint64(len(oldSerialized[offset:])) < numBitmapBytes {
+				return errDeserialize("unexpected end of data for " +
+					"unspentness bitmap")
+			}
+
+			// Add sparse outputs for unspent outputs 0 and 1 as needed based on the
+			// details provided by the header code.
+			var outputIndexes []uint32
+			if output0Unspent {
+				outputIndexes = append(outputIndexes, 0)
+			}
+			if output1Unspent {
+				outputIndexes = append(outputIndexes, 1)
+			}
+
+			// Decode the unspentness bitmap adding a sparse output for each unspent
+			// output.
+			for i := uint32(0); i < uint32(numBitmapBytes); i++ {
+				unspentBits := oldSerialized[offset]
+				for j := uint32(0); j < 8; j++ {
+					if unspentBits&0x01 != 0 {
+						// The first 2 outputs are encoded via the
+						// header code, so adjust the output number
+						// accordingly.
+						outputNum := 2 + i*8 + j
+						outputIndexes = append(outputIndexes, outputNum)
+					}
+					unspentBits >>= 1
+				}
+				offset++
+			}
+
+			// Create a map to hold all of the converted outputs for the entry.
+			type convertedOut struct {
+				compressedAmount uint64
+				compressedScript []byte
+				scriptVersion    uint64
+			}
+			outputs := make(map[uint32]*convertedOut)
+
+			// Decode and add all of the outputs.
+			for _, outputIndex := range outputIndexes {
+				// Deserialize the compressed amount and ensure there are bytes
+				// remaining for the compressed script.
+				compressedAmount, bytesRead := deserializeVLQ(oldSerialized[offset:])
+				if bytesRead == 0 {
+					return errDeserialize("unexpected end of data during decoding " +
+						"(compressed amount)")
+				}
+				offset += bytesRead
+
+				// Decode the script version.
+				scriptVersion, bytesRead := deserializeVLQ(oldSerialized[offset:])
+				if bytesRead == 0 {
+					return errDeserialize("unexpected end of data during decoding " +
+						"(script version)")
+				}
+				offset += bytesRead
+
+				// Decode the compressed script size and ensure there are enough bytes
+				// left in the slice for it.
+				const compressionVersion = 1
+				scriptSize := decodeCompressedScriptSize(oldSerialized[offset:],
+					compressionVersion)
+				// Note: scriptSize == 0 is OK (an empty compressed script is valid)
+				if scriptSize < 0 {
+					return errDeserialize("negative script size")
+				}
+				if len(oldSerialized[offset:]) < scriptSize {
+					return errDeserialize(fmt.Sprintf("unexpected end of "+
+						"data after script size (got %v, need %v)",
+						len(oldSerialized[offset:]), scriptSize))
+				}
+				compressedScript := oldSerialized[offset : offset+scriptSize]
+
+				offset += scriptSize
+
+				// Create a converted utxo entry with the details deserialized above.
+				outputs[outputIndex] = &convertedOut{
+					compressedAmount: compressedAmount,
+					compressedScript: compressedScript,
+					scriptVersion:    scriptVersion,
+				}
+			}
+			// Read the minimal outputs if this was a ticket.
+			var ticketMinOuts []byte
+			if txType == stake.TxTypeSStx {
+				sz, err := determineMinimalOutputsSizeV1(oldSerialized[offset:])
+				if err != nil {
+					return errDeserialize(fmt.Sprintf("unable to decode "+
+						"ticket outputs: %v", err))
+				}
+
+				// Read the ticket minimal outputs.
+				ticketMinOuts = oldSerialized[offset : offset+sz]
+			}
+
+			// Create V3 utxo entries with the details deserialized above.
+			//
+			// The V3 serialized key format is:
+			//
+			//   <hash><tree><output index>
+			//
+			//   Field                Type             Size
+			//   hash                 chainhash.Hash   chainhash.HashSize
+			//   tree                 VLQ              variable
+			//   output index         VLQ              variable
+			//
+			// The V3 serialized value format is:
+			//
+			//   <block height><block index><flags><compressed txout>
+			//   OPTIONAL: [<ticket min outs>]
+			//
+			//   Field                Type     Size
+			//   block height         VLQ      variable
+			//   block index          VLQ      variable
+			//   flags                VLQ      variable
+			//   compressed txout
+			//     compressed amount   VLQ      variable
+			//     script version      VLQ      variable
+			//     compressed script   []byte   variable
+			//
+			//   OPTIONAL
+			//     ticketMinOuts      []byte         variable
+			//
+			// The serialized flags format is:
+			//   bit  0     - containing transaction is a coinbase
+			//   bit  1     - containing transaction has an expiry
+			//   bits 2-5   - transaction type
+			//   bits 6-7   - unused
+			//
+			// The ticket min outs field contains minimally encoded outputs for all
+			// outputs of a ticket transaction. It is only encoded for ticket outputs.
+			for outputIndex, output := range outputs {
+				// Encode the V3 utxo flags.
+				encodedFlags := uint8(txType) << 2
+				if isCoinBase {
+					encodedFlags |= 1
+				}
+				if hasExpiry {
+					encodedFlags |= 1 << 1
+				}
+
+				// Calculate the size needed to serialize the entry.
+				size := serializeSizeVLQ(blockHeight) +
+					serializeSizeVLQ(blockIndex) +
+					serializeSizeVLQ(uint64(encodedFlags)) +
+					serializeSizeVLQ(output.compressedAmount) +
+					serializeSizeVLQ(output.scriptVersion) +
+					len(output.compressedScript)
+
+				// Only store the ticket minimal outputs in the ticket submission
+				// output (output index 0).
+				if outputIndex == 0 {
+					size += len(ticketMinOuts)
+				}
+
+				// Serialize the entry.
+				reserialized := make([]byte, size)
+				reserializedOffset := putVLQ(reserialized, blockHeight)
+				reserializedOffset += putVLQ(reserialized[reserializedOffset:],
+					blockIndex)
+				reserializedOffset += putVLQ(reserialized[reserializedOffset:],
+					uint64(encodedFlags))
+				reserializedOffset += putVLQ(reserialized[reserializedOffset:],
+					output.compressedAmount)
+				reserializedOffset += putVLQ(reserialized[reserializedOffset:],
+					output.scriptVersion)
+				copy(reserialized[reserializedOffset:], output.compressedScript)
+				reserializedOffset += len(output.compressedScript)
+
+				// Only store the ticket minimal outputs in the ticket submission
+				// output (output index 0).
+				if ticketMinOuts != nil && outputIndex == 0 {
+					copy(reserialized[reserializedOffset:], ticketMinOuts)
+				}
+
+				// Create the key for the new entry.
+				tree := wire.TxTreeRegular
+				if txType != stake.TxTypeRegular {
+					tree = wire.TxTreeStake
+				}
+				keySize := chainhash.HashSize + serializeSizeVLQ(uint64(tree)) +
+					serializeSizeVLQ(uint64(outputIndex))
+				key := make([]byte, keySize)
+				copy(key, txHash[:])
+				keyOffset := chainhash.HashSize
+				keyOffset += putVLQ(key[keyOffset:], uint64(tree))
+				putVLQ(key[keyOffset:], uint64(outputIndex))
+
+				// Create the new entry in the V3 bucket.
+				err = v3Bucket.Put(key, reserialized)
+				if err != nil {
+					return err
+				}
+			}
+
+			numMigrated++
+			return nil
+		})
+		isFullyDone := err == nil
+		if (isFullyDone || logProgress) && numMigrated > 0 {
+			totalMigrated += uint64(numMigrated)
+			log.Infof("Migrated %d entries (%d total)", numMigrated,
+				totalMigrated)
+		}
+		return isFullyDone, err
+	}
+
+	// Migrate all entries in batches for the reasons mentioned above.
+	if err := batchedUpdate(ctx, db, doBatch); err != nil {
+		return err
+	}
+
+	elapsed := time.Since(start).Round(time.Millisecond)
+	log.Infof("Done migrating utxoset.  Total entries: %d in %v", totalMigrated,
+		elapsed)
+
+	if interruptRequested(ctx) {
+		return errInterruptRequested
+	}
+
+	// Drop version 2 utxoset.
+	log.Info("Removing old utxoset entries...")
+	start = time.Now()
+	err = incrementalFlatDrop(ctx, db, v2BucketName, "old utxoset")
+	if err != nil {
+		return err
+	}
+	elapsed = time.Since(start).Round(time.Millisecond)
+	log.Infof("Done removing old utxoset entries in %v", elapsed)
+	return nil
+}
+
+// migrateSpendJournalVersion2To3 migrates all spend journal entries from the v2
+// bucket to a v3 bucket and removes the old v2 bucket.
+//
+// The spend journal entries are guaranteed to be fully updated if this returns
+// without failure.
+func migrateSpendJournalVersion2To3(ctx context.Context, b *BlockChain) error {
+	// Hardcoded bucket and key names so updates do not affect old upgrades.
+	v2SpendJournalBucketName := []byte("spendjournalv2")
+	v3SpendJournalBucketName := []byte("spendjournalv3")
+	v3BlockIndexBucketName := []byte("blockidxv3")
+	v3UtxoSetBucketName := []byte("utxosetv3")
+	tmpTxInfoBucketName := []byte("tmpTxInfo")
+
+	log.Info("Migrating database spend journal.  This may take a while...")
+	start := time.Now()
+
+	// Create the new spend journal bucket as needed.
+	err := b.db.Update(func(dbTx database.Tx) error {
+		_, err := dbTx.Metadata().CreateBucketIfNotExists(v3SpendJournalBucketName)
+		return err
+	})
+	if err != nil {
+		return err
+	}
+
+	// Create the temp bucket as needed.  This bucket is used to temporarily store
+	// tx info for fully spent transactions.
+	err = b.db.Update(func(dbTx database.Tx) error {
+		_, err := dbTx.Metadata().CreateBucketIfNotExists(tmpTxInfoBucketName)
+		return err
+	})
+	if err != nil {
+		return err
+	}
+
+	// doBatch contains the primary logic for upgrading the spend journal from
+	// version 2 to 3 in batches.  This is done because attempting to migrate in a
+	// single database transaction could result in massive memory usage and
+	// could potentially crash on many systems due to ulimits.
+	//
+	// It returns whether or not all entries have been updated.
+	const maxEntries = 20000
+	var resumeOffset uint32
+	var totalMigrated uint64
+	doBatch := func(dbTx database.Tx) (bool, error) {
+		meta := dbTx.Metadata()
+
+		v2SpendJournalBucket := meta.Bucket(v2SpendJournalBucketName)
+		if v2SpendJournalBucket == nil {
+			return false, fmt.Errorf("bucket %s does not exist",
+				v2SpendJournalBucketName)
+		}
+
+		v3SpendJournalBucket := meta.Bucket(v3SpendJournalBucketName)
+		if v3SpendJournalBucket == nil {
+			return false, fmt.Errorf("bucket %s does not exist",
+				v3SpendJournalBucketName)
+		}
+
+		v3BlockIndexBucket := meta.Bucket(v3BlockIndexBucketName)
+		if v3BlockIndexBucket == nil {
+			return false, fmt.Errorf("bucket %s does not exist",
+				v3BlockIndexBucketName)
+		}
+
+		v3UtxoSetBucket := meta.Bucket(v3UtxoSetBucketName)
+		if v3UtxoSetBucket == nil {
+			return false, fmt.Errorf("bucket %s does not exist",
+				v3UtxoSetBucketName)
+		}
+
+		tmpTxInfoBucket := meta.Bucket(tmpTxInfoBucketName)
+		if tmpTxInfoBucket == nil {
+			return false, fmt.Errorf("bucket %s does not exist",
+				tmpTxInfoBucketName)
+		}
+
+		// Migrate spend journal entries so long as the max number of entries for
+		// this batch has not been exceeded.
+		//
+		// Use the block index to iterate through every block in reverse order.  It
+		// is necessary to read the blocks in reverse order since the V2 spend
+		// journal conditionally stored tx info when the output was the last spent
+		// output of the containing transaction.  Therefore, we need to find the
+		// last spent outputs first and temporarily store the associated tx info so
+		// that it is available when creating the corresponding V3 entries.
+		var logProgress bool
+		var numMigrated, numIterated uint32
+		cursor := v3BlockIndexBucket.Cursor()
+		for ok := cursor.Last(); ok; ok = cursor.Prev() {
+			// Reset err on each iteration.
+			err = nil
+
+			if interruptRequested(ctx) {
+				logProgress = true
+				err = errInterruptRequested
+				break
+			}
+
+			if numMigrated >= maxEntries {
+				logProgress = true
+				err = errBatchFinished
+				break
+			}
+
+			// Skip entries that have already been migrated in previous batches.
+			numIterated++
+			if numIterated-1 < resumeOffset {
+				continue
+			}
+			resumeOffset++
+
+			// Deserialize the header from the V3 block index entry.  The header is
+			// the first item in the serialized entry.
+			serializedBlockIndexEntry := cursor.Value()
+			if len(serializedBlockIndexEntry) < blockHdrSize {
+				return false, errDeserialize("unexpected end of data while " +
+					"reading block header")
+			}
+			hB := serializedBlockIndexEntry[0:blockHdrSize]
+			var header wire.BlockHeader
+			if err := header.Deserialize(bytes.NewReader(hB)); err != nil {
+				return false, err
+			}
+
+			// Skip entries that have already been migrated in previous interrupted
+			// upgrades.
+			blockHash := header.BlockHash()
+			if v3SpendJournalBucket.Get(blockHash[:]) != nil {
+				continue
+			}
+
+			// Load the full block from the database.
+			blockBytes, err := dbTx.FetchBlock(&blockHash)
+			if err != nil {
+				break
+			}
+			var msgBlock wire.MsgBlock
+			if err = msgBlock.FromBytes(blockBytes); err != nil {
+				break
+			}
+
+			// Determine if treasury agenda is active.
+			isTreasuryEnabled := false
+			if msgBlock.Header.Height > 0 {
+				parentHash := msgBlock.Header.PrevBlock
+				isTreasuryEnabled, err = b.IsTreasuryAgendaActive(&parentHash)
+				if err != nil {
+					break
+				}
+			}
+
+			// Deserialize the legacy V2 spend journal entry.
+			//
+			// The legacy V2 format is as follows:
+			//
+			//   [<flags><script version><compressed pk script>],...
+			//   OPTIONAL: [<txVersion><stakeExtra>]
+			//
+			//   Field                Type           Size
+			//   flags                VLQ            byte
+			//   scriptVersion        uint16         2 bytes
+			//   pkScript             VLQ+[]byte     variable
+			//
+			//   OPTIONAL
+			//     txVersion          VLQ            variable
+			//     stakeExtra         []byte         variable
+			//
+			// The serialized flags code format is:
+			//   bit  0   - containing transaction is a coinbase
+			//   bit  1   - containing transaction has an expiry
+			//   bits 2-4 - transaction type
+			//   bit  5   - unused
+			//   bit  6   - is fully spent
+			//   bit  7   - unused
+			//
+			// The stake extra field contains minimally encoded outputs for all
+			// consensus-related outputs in the stake transaction. It is only
+			// encoded for tickets.
+			//
+			//   NOTE: The transaction version and flags are only encoded when the
+			//   spent txout was the final unspent output of the containing
+			//   transaction.  Otherwise, the header code will be 0 and the version is
+			//   not serialized at all. This is done because that information is only
+			//   needed when the utxo set no longer has it.
+
+			// Get the serialized V2 data.
+			v2Serialized := v2SpendJournalBucket.Get(blockHash[:])
+
+			// Continue if there is no spend journal entry for the block.
+			if v2Serialized == nil {
+				continue
+			}
+
+			// Exclude the coinbase transaction since it can't spend anything.
+			blockTxns := make([]*wire.MsgTx, 0, len(msgBlock.STransactions)+
+				len(msgBlock.Transactions[1:]))
+			if len(msgBlock.STransactions) > 0 && isTreasuryEnabled {
+				// Skip treasury base and remove tspends.
+				for _, v := range msgBlock.STransactions[1:] {
+					if stake.IsTSpend(v) {
+						continue
+					}
+					blockTxns = append(blockTxns, v)
+				}
+			} else {
+				blockTxns = append(blockTxns, msgBlock.STransactions...)
+			}
+			blockTxns = append(blockTxns, msgBlock.Transactions[1:]...)
+
+			// Calculate the total number of stxos.
+			var numStxos int
+			for _, tx := range blockTxns {
+				if stake.IsSSGen(tx, isTreasuryEnabled) {
+					numStxos++
+					continue
+				}
+				numStxos += len(tx.TxIn)
+			}
+
+			// If there is an empty spend journal entry for the block, create an empty
+			// entry in the V3 bucket and continue.
+			if len(v2Serialized) == 0 {
+				// Ensure the block actually has no stxos.  This should never
+				// happen unless there is database corruption or an empty entry
+				// erroneously made its way into the database.
+				if numStxos != 0 {
+					return false, AssertError(fmt.Sprintf("mismatched spend "+
+						"journal serialization - no serialization for "+
+						"expected %d stxos", numStxos))
+				}
+
+				err = v3SpendJournalBucket.Put(blockHash[:], nil)
+				if err != nil {
+					return false, err
+				}
+
+				continue
+			}
+
+			// Create a slice to hold all of the converted stxos.
+			type convertedStxo struct {
+				compScript    []byte
+				ticketMinOuts []byte
+				scriptVersion uint16
+				flags         uint8
+				txOutIndex    uint32
+			}
+			stxos := make([]convertedStxo, numStxos)
+
+			// Loop backwards through all transactions so everything is read in
+			// reverse order to match the serialization order.
+			stxoIdx := numStxos - 1
+			offset := 0
+			for txIdx := len(blockTxns) - 1; txIdx > -1; txIdx-- {
+				tx := blockTxns[txIdx]
+				isVote := stake.IsSSGen(tx, isTreasuryEnabled)
+
+				// Loop backwards through all of the transaction inputs and read
+				// the associated stxo.
+				for txInIdx := len(tx.TxIn) - 1; txInIdx > -1; txInIdx-- {
+					// Skip stakebase since it has no input.
+					if txInIdx == 0 && isVote {
+						continue
+					}
+
+					txIn := tx.TxIn[txInIdx]
+					stxo := &stxos[stxoIdx]
+					stxoIdx--
+
+					// Set the tx out index that the stxo is associated with.
+					stxo.txOutIndex = txIn.PreviousOutPoint.Index
+
+					// Deserialize the flags.
+					flags, bytesRead := deserializeVLQ(v2Serialized[offset:])
+					if bytesRead == 0 {
+						return false, errDeserialize("unexpected end of data during " +
+							"decoding (flags)")
+					}
+					offset += bytesRead
+
+					// Decode the script version.
+					scriptVersion, bytesRead := deserializeVLQ(v2Serialized[offset:])
+					if bytesRead == 0 {
+						return false, errDeserialize("unexpected end of " +
+							"data during decoding (script version)")
+					}
+					offset += bytesRead
+					stxo.scriptVersion = uint16(scriptVersion)
+
+					// Decode the compressed script size and ensure there are enough bytes
+					// left in the slice for it.
+					const compressionVersion = 1
+					scriptSize := decodeCompressedScriptSize(v2Serialized[offset:],
+						compressionVersion)
+					// Note: scriptSize == 0 is OK (an empty compressed script is valid)
+					if scriptSize < 0 {
+						return false, errDeserialize("negative script size")
+					}
+					if len(v2Serialized[offset:]) < scriptSize {
+						return false, errDeserialize(fmt.Sprintf("unexpected end of "+
+							"data after script size (got %v, need %v)",
+							len(v2Serialized[offset:]), scriptSize))
+					}
+					compressedScript := v2Serialized[offset : offset+scriptSize]
+					offset += scriptSize
+					stxo.compScript = compressedScript
+
+					// Deserialize the containing transaction if the flags indicate that
+					// the transaction has been fully spent.
+					// The flags format is:
+					//   bit  0   - containing transaction is a coinbase
+					//   bit  1   - containing transaction has an expiry
+					//   bits 2-4 - transaction type
+					//   bit  5   - unused
+					//   bit  6   - is fully spent
+					//   bit  7   - unused
+					fullySpent := flags&(1<<6) != 0
+					if fullySpent {
+						txType := stake.TxType((flags & 0x1c) >> 2)
+						stxo.flags = uint8(flags)
+
+						// Unset the spent flag since it is no longer needed in the new
+						// version.
+						stxo.flags &^= 1 << 6
+
+						// Deserialize the version and ignore it since it is no longer
+						// used in the new format.
+						_, bytesRead := deserializeVLQ(v2Serialized[offset:])
+						if bytesRead == 0 {
+							return false, errDeserialize("unexpected end of " +
+								"data during decoding (tx version)")
+						}
+						offset += bytesRead
+
+						if txType == stake.TxTypeSStx {
+							sz, err := determineMinimalOutputsSizeV1(v2Serialized[offset:])
+							if err != nil {
+								return false, errDeserialize(fmt.Sprintf("unable to decode "+
+									"ticket outputs: %v", err))
+							}
+
+							// Read the ticket minimal outputs.
+							stxo.ticketMinOuts = v2Serialized[offset : offset+sz]
+							offset += sz
+						}
+
+						// Save the fully spent tx info in the temp bucket.  Store the
+						// fields that were conditionally stored in V2, which includes flags
+						// and ticket minimal outputs.
+						//
+						// The serialzied format is:
+						//   flags             VLQ            byte
+						//   ticketMinOuts     []byte         variable
+						//
+						// The flags format is:
+						//   bit  0   - containing transaction is a coinbase
+						//   bit  1   - containing transaction has an expiry
+						//   bits 2-5 - transaction type
+						//   bits 6-7 - unused
+						//
+						// The ticket min outs field is only stored for tickets.
+						size := serializeSizeVLQ(uint64(stxo.flags))
+						if txType == stake.TxTypeSStx {
+							size += len(stxo.ticketMinOuts)
+						}
+						target := make([]byte, size)
+						tmpOffset := putVLQ(target, uint64(stxo.flags))
+						if txType == stake.TxTypeSStx {
+							copy(target[tmpOffset:], stxo.ticketMinOuts)
+						}
+						err = tmpTxInfoBucket.Put(txIn.PreviousOutPoint.Hash[:], target)
+						if err != nil {
+							return false, err
+						}
+
+						// Continue since we have everything we need for stxos that were
+						// marked as fully spent.
+						continue
+					}
+
+					// The stxo was not marked as fully spent (otherwise we already
+					// continued above).  To get the missing tx info (flags and ticket
+					// min outs), first check the tmp bucket, and if not found, then
+					// check the utxo set.
+
+					// First, check the temp bucket for the tx info.
+					tmpSerialized := tmpTxInfoBucket.Get(txIn.PreviousOutPoint.Hash[:])
+					if len(tmpSerialized) != 0 {
+						flags, bytesRead := deserializeVLQ(tmpSerialized)
+						tOffset := bytesRead
+						stxo.flags = uint8(flags)
+
+						// Decode the flags.  The format is:
+						//   bit  0   - containing transaction is a coinbase
+						//   bit  1   - containing transaction has an expiry
+						//   bits 2-5 - transaction type
+						//   bits 6-7 - unused
+						txType := stake.TxType((flags & 0x3c) >> 2)
+						// Read the minimal outputs if this was a ticket submission output.
+						if txType == stake.TxTypeSStx && stxo.txOutIndex == 0 {
+							sz, err := determineMinimalOutputsSizeV1(tmpSerialized[tOffset:])
+							if err != nil {
+								return false, errDeserialize(fmt.Sprintf("unable to decode "+
+									"ticket outputs: %v", err))
+							}
+
+							// Read the ticket minimal outputs.
+							stxo.ticketMinOuts = tmpSerialized[tOffset : tOffset+sz]
+						}
+
+						// Continue to the next stxo since the flags and ticket min outs
+						// have now been set.
+						continue
+					}
+
+					// If the temp bucket didn't have the tx info, check the utxo set for
+					// the tx info.  We don't know what output to look for, so this scans
+					// until it finds the first output for the corresponding transaction
+					// hash.
+					//
+					// Attempt to find an entry by seeking for the hash along with a zero
+					// index and tree.  Due to the fact the keys are serialized as
+					// <hash><tree><index>, where the tree and index use an MSB encoding,
+					// if there are any entries for the hash at all, one will be found.
+					cursor := v3UtxoSetBucket.Cursor()
+					tree := uint64(0)
+					index := uint64(0)
+					utxoKey := make([]byte, chainhash.HashSize+serializeSizeVLQ(tree)+
+						serializeSizeVLQ(index))
+					copy(utxoKey, txIn.PreviousOutPoint.Hash[:])
+					utxoKeyOffset := chainhash.HashSize
+					utxoKeyOffset += putVLQ(utxoKey[utxoKeyOffset:], tree)
+					putVLQ(utxoKey[utxoKeyOffset:], index)
+					ok := cursor.Seek(utxoKey)
+					if !ok {
+						return false, AssertError(fmt.Sprintf("Couldn't find any utxo "+
+							"for non-fully spent tx %v",
+							txIn.PreviousOutPoint.Hash.String()))
+					}
+					cursorKey := cursor.Key()
+					if len(cursorKey) < chainhash.HashSize {
+						return false, AssertError(fmt.Sprintf("Couldn't find any utxo "+
+							"for non-fully spent tx %v",
+							txIn.PreviousOutPoint.Hash.String()))
+					}
+
+					// An entry was found, but it could just be an entry with the next
+					// highest hash after the requested one, so make sure the hashes
+					// actually match.
+					if chainhash.HashSize >= len(cursorKey) {
+						return false, errDeserialize("unexpected length for serialized " +
+							"outpoint key")
+					}
+					var keyHash chainhash.Hash
+					copy(keyHash[:], cursorKey[:chainhash.HashSize])
+					keyOffset := chainhash.HashSize
+					keyTree, bytesRead := deserializeVLQ(cursorKey[keyOffset:])
+					keyOffset += bytesRead
+					if keyOffset >= len(cursorKey) {
+						return false, errDeserialize("unexpected end of data after tree")
+					}
+					keyIdx, _ := deserializeVLQ(cursorKey[keyOffset:])
+					outpoint := wire.OutPoint{Hash: keyHash, Index: uint32(keyIdx),
+						Tree: int8(keyTree)}
+					if outpoint.Hash != txIn.PreviousOutPoint.Hash {
+						return false, AssertError(fmt.Sprintf("Couldn't find any utxo "+
+							"for non-fully spent tx %v",
+							txIn.PreviousOutPoint.Hash.String()))
+					}
+
+					// Deserialize the V3 utxo entry.
+					//
+					// The V3 serialized key format is:
+					//
+					//   <hash><tree><output index>
+					//
+					//   Field                Type             Size
+					//   hash                 chainhash.Hash   chainhash.HashSize
+					//   tree                 VLQ              variable
+					//   output index         VLQ              variable
+					//
+					// The V3 serialized value format is:
+					//
+					//   <block height><block index><flags><compressed txout>
+					//   OPTIONAL: [<ticket min outs>]
+					//
+					//   Field                Type     Size
+					//   block height         VLQ      variable
+					//   block index          VLQ      variable
+					//   flags                VLQ      variable
+					//   compressed txout
+					//     compressed amount   VLQ      variable
+					//     script version      VLQ      variable
+					//     compressed script   []byte   variable
+					//
+					//   OPTIONAL
+					//     ticketMinOuts      []byte         variable
+					//
+					// The serialized flags format is:
+					//   bit  0     - containing transaction is a coinbase
+					//   bit  1     - containing transaction has an expiry
+					//   bits 2-5   - transaction type
+					//   bits 6-7   - unused
+					//
+					// The ticket min outs field contains minimally encoded outputs for
+					// all outputs of a ticket transaction. It is only encoded for ticket
+					// submission outputs.
+					utxoSerialized := cursor.Value()
+
+					// Deserialize the block height.  Ignore it since we don't need it.
+					_, bytesRead = deserializeVLQ(utxoSerialized)
+					utxoOffset := bytesRead
+					if utxoOffset >= len(utxoSerialized) {
+						return false, errDeserialize("unexpected end of data after height")
+					}
+
+					// Deserialize the block index.  Ignore it since we don't need it.
+					_, bytesRead = deserializeVLQ(utxoSerialized[utxoOffset:])
+					utxoOffset += bytesRead
+					if utxoOffset >= len(utxoSerialized) {
+						return false, errDeserialize("unexpected end of data after index")
+					}
+
+					// Deserialize the flags.
+					v3UtxoFlags, bytesRead := deserializeVLQ(utxoSerialized[utxoOffset:])
+					utxoOffset += bytesRead
+					if utxoOffset >= len(utxoSerialized) {
+						return false, errDeserialize("unexpected end of data after flags")
+					}
+
+					// Set the flags on the stxo.
+					stxo.flags = uint8(v3UtxoFlags)
+
+					// Deserialize the compressed amount and ensure there are bytes
+					// remaining for the compressed script.  Ignore it since we don't need
+					// it.
+					_, bytesRead = deserializeVLQ(utxoSerialized[utxoOffset:])
+					if bytesRead == 0 {
+						return false, errDeserialize("unexpected end of data during " +
+							"decoding (compressed amount)")
+					}
+					utxoOffset += bytesRead
+
+					// Decode the script version.  Ignore it since we don't need it.
+					_, bytesRead = deserializeVLQ(utxoSerialized[utxoOffset:])
+					if bytesRead == 0 {
+						return false, errDeserialize("unexpected end of data during " +
+							"decoding (script version)")
+					}
+					utxoOffset += bytesRead
+
+					// Decode the compressed script size and ensure there are enough bytes
+					// left in the slice for it.
+					scriptSize = decodeCompressedScriptSize(utxoSerialized[utxoOffset:],
+						compressionVersion)
+					// Note: scriptSize == 0 is OK (an empty compressed script is valid)
+					if scriptSize < 0 {
+						return false, errDeserialize("negative script size")
+					}
+					if len(utxoSerialized[utxoOffset:]) < scriptSize {
+						return false, errDeserialize(fmt.Sprintf("unexpected end of "+
+							"data after script size (got %v, need %v)",
+							len(utxoSerialized[utxoOffset:]), scriptSize))
+					}
+					// Ignore the script since we don't need it.
+					utxoOffset += scriptSize
+
+					// Determine the tx type from the flags.  The flags format is:
+					//   bit  0     - containing transaction is a coinbase
+					//   bit  1     - containing transaction has an expiry
+					//   bits 2-5   - transaction type
+					//   bits 6-7   - unused
+					txType := stake.TxType((v3UtxoFlags & 0x3c) >> 2)
+
+					// Read the minimal outputs if this was a ticket submission output.
+					if txType == stake.TxTypeSStx && outpoint.Index == 0 {
+						sz, err := determineMinimalOutputsSizeV1(utxoSerialized[utxoOffset:])
+						if err != nil {
+							return false, errDeserialize(fmt.Sprintf("unable to decode "+
+								"ticket outputs: %v", err))
+						}
+
+						// Read the ticket minimal outputs.
+						stxo.ticketMinOuts = utxoSerialized[utxoOffset : utxoOffset+sz]
+					}
+				}
+			}
+
+			// Create a V3 spend journal entry with the details deserialized above.
+			//
+			// The V3 serialized format is:
+			//
+			//   [<flags><script version><compressed pk script>],...
+			//   OPTIONAL: <ticket min outs>
+			//
+			//   Field                Type           Size
+			//   flags                VLQ            byte
+			//   scriptVersion        uint16         2 bytes
+			//   pkScript             VLQ+[]byte     variable
+			//
+			//   OPTIONAL
+			//     ticketMinOuts      []byte         variable
+			//
+			// The serialized flags format is:
+			//   bit  0     - containing transaction is a coinbase
+			//   bit  1     - containing transaction has an expiry
+			//   bits 2-5   - transaction type
+			//   bits 6-7   - unused
+			//
+			// The ticket min outs field contains minimally encoded outputs for all
+			// outputs of a ticket transaction. It is only encoded for ticket
+			// submission outputs.
+
+			// Calculate the size needed to serialize the entire journal entry.
+			var size int
+			sizes := make([]int, 0, len(stxos))
+			for i := range stxos {
+				stxo := &stxos[i]
+				sz := serializeSizeVLQ(uint64(stxo.flags)) +
+					serializeSizeVLQ(uint64(stxo.scriptVersion)) +
+					len(stxo.compScript)
+
+				// Determine the tx type from the flags.  The flags format is:
+				//   bit  0     - containing transaction is a coinbase
+				//   bit  1     - containing transaction has an expiry
+				//   bits 2-5   - transaction type
+				//   bits 6-7   - unused
+				txType := stake.TxType((stxo.flags & 0x3c) >> 2)
+
+				// Only store the minimal outputs if this was a ticket submission
+				// output.
+				if txType == stake.TxTypeSStx && stxo.txOutIndex == 0 {
+					sz += len(stxo.ticketMinOuts)
+				}
+
+				sizes = append(sizes, sz)
+				size += sz
+			}
+			reserialized := make([]byte, size)
+
+			// Serialize each individual stxo directly into the slice in reverse
+			// order one after the other.
+			offset = 0
+			for i := len(stxos) - 1; i > -1; i-- {
+				oldOffset := offset
+				stxo := &stxos[i]
+				offset += putVLQ(reserialized[offset:], uint64(stxo.flags))
+				offset += putVLQ(reserialized[offset:], uint64(stxo.scriptVersion))
+				copy(reserialized[offset:], stxo.compScript)
+				offset += len(stxo.compScript)
+
+				// Determine the tx type from the flags.  The flags format is:
+				//   bit  0     - containing transaction is a coinbase
+				//   bit  1     - containing transaction has an expiry
+				//   bits 2-5   - transaction type
+				//   bits 6-7   - unused
+				txType := stake.TxType((stxo.flags & 0x3c) >> 2)
+
+				// Only store the minimal outputs if this was a ticket submission
+				// output.
+				if txType == stake.TxTypeSStx && stxo.txOutIndex == 0 {
+					copy(reserialized[offset:], stxo.ticketMinOuts)
+					offset += len(stxo.ticketMinOuts)
+				}
+
+				if offset-oldOffset != sizes[i] {
+					return false, AssertError(fmt.Sprintf("bad write; expect sz %v, "+
+						"got sz %v (wrote %x)", sizes[i], offset-oldOffset,
+						reserialized[oldOffset:offset]))
+				}
+			}
+
+			// Create the new entry in the V3 bucket.
+			err = v3SpendJournalBucket.Put(blockHash[:], reserialized)
+			if err != nil {
+				return false, err
+			}
+
+			numMigrated++
+		}
+		isFullyDone := err == nil
+		if (isFullyDone || logProgress) && numMigrated > 0 {
+			totalMigrated += uint64(numMigrated)
+			log.Infof("Migrated %d entries (%d total)", numMigrated,
+				totalMigrated)
+		}
+		return isFullyDone, err
+	}
+
+	// Migrate all entries in batches for the reasons mentioned above.
+	if err := batchedUpdate(ctx, b.db, doBatch); err != nil {
+		return err
+	}
+
+	elapsed := time.Since(start).Round(time.Millisecond)
+	log.Infof("Done migrating spend journal.  Total entries: %d in %v",
+		totalMigrated, elapsed)
+
+	if interruptRequested(ctx) {
+		return errInterruptRequested
+	}
+
+	// Drop temp bucket.
+	log.Info("Removing temp data...")
+	start = time.Now()
+	err = incrementalFlatDrop(ctx, b.db, tmpTxInfoBucketName, "temp data")
+	if err != nil {
+		return err
+	}
+	elapsed = time.Since(start).Round(time.Millisecond)
+	log.Infof("Done removing temp data in %v", elapsed)
+
+	// Drop version 2 spend journal.
+	log.Info("Removing old spend journal...")
+	start = time.Now()
+	err = incrementalFlatDrop(ctx, b.db, v2SpendJournalBucketName, "old journal")
+	if err != nil {
+		return err
+	}
+	elapsed = time.Since(start).Round(time.Millisecond)
+	log.Infof("Done removing old spend journal in %v", elapsed)
+	return nil
+}
+
+// upgradeToVersion9 upgrades a version 8 blockchain database to version 9.
+func upgradeToVersion9(ctx context.Context, b *BlockChain) error {
+	if interruptRequested(ctx) {
+		return errInterruptRequested
+	}
+
+	log.Info("Upgrading database to version 9...")
+	start := time.Now()
+
+	// Migrate the utxoset to version 3.
+	v3DoneKeyName := []byte("utxosetv3done")
+	err := runUpgradeStageOnce(ctx, b.db, v3DoneKeyName, func() error {
+		return migrateUtxoSetVersion2To3(ctx, b.db)
+	})
+	if err != nil {
+		return err
+	}
+
+	if interruptRequested(ctx) {
+		return errInterruptRequested
+	}
+
+	// Migrate the spend journal to version 3.
+	err = migrateSpendJournalVersion2To3(ctx, b)
+	if err != nil {
+		return err
+	}
+
+	// Update and persist the database version  and remove upgrade progress
+	// tracking keys.
+	err = b.db.Update(func(dbTx database.Tx) error {
+		err := dbTx.Metadata().Delete(v3DoneKeyName)
+		if err != nil {
+			return err
+		}
+
+		b.dbInfo.version = 9
+		return dbPutDatabaseInfo(dbTx, b.dbInfo)
+	})
+	if err != nil {
+		return err
+	}
+
+	elapsed := time.Since(start).Round(time.Millisecond)
+	log.Infof("Done upgrading database in %v.", elapsed)
+	return nil
+}
+
 // checkDBTooOldToUpgrade returns an ErrDBTooOldToUpgrade error if the provided
 // database version can no longer be upgraded due to being too old.
 func checkDBTooOldToUpgrade(dbVersion uint32) error {
@@ -1830,7 +3008,9 @@ func CheckDBTooOldToUpgrade(db database.DB) error {
 }
 
 // upgradeDB upgrades old database versions to the newest version by applying
-// all possible upgrades iteratively.
+// all possible upgrades iteratively.  Note that there may also be upgrades
+// that run after the block index has been loaded as part of the
+// upgradeDBPostBlockIndexLoad function.
 //
 // NOTE: The passed database info will be updated with the latest versions.
 func upgradeDB(ctx context.Context, db database.DB, chainParams *chaincfg.Params, dbInfo *databaseInfo) error {
@@ -1888,3 +3068,22 @@ func upgradeDB(ctx context.Context, db database.DB, chainParams *chaincfg.Params
 
 	return nil
 }
+
+// upgradeDBPostBlockIndexLoad upgrades old database versions to the newest
+// version by applying all possible post block index upgrades iteratively.
+// This function should only include upgrades that need to be run after the
+// block index has been loaded.  Upgrades that run prior to the block index
+// being loaded should be included in the upgradeDB function.
+//
+// NOTE: The passed database info will be updated with the latest versions.
+func upgradeDBPostBlockIndexLoad(ctx context.Context, b *BlockChain) error {
+	// Update to a version 9 database if needed.  This entails migrating the
+	// utxoset and spend journal to the v3 format.
+	if b.dbInfo.version == 8 {
+		if err := upgradeToVersion9(ctx, b); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/blockchain/utxoviewpoint.go b/blockchain/utxoviewpoint.go
index 36e0be1a..9169ded7 100644
--- a/blockchain/utxoviewpoint.go
+++ b/blockchain/utxoviewpoint.go
@@ -17,227 +17,206 @@ import (
 	"github.com/decred/dcrd/wire"
 )
 
-// utxoOutput houses details about an individual unspent transaction output such
-// as whether or not it is spent, its public key script, and how much it pays.
-//
-// Standard public key scripts are stored in the database using a compressed
-// format. Since the vast majority of scripts are of the standard form, a fairly
-// significant savings is achieved by discarding the portions of the standard
-// scripts that can be reconstructed.
-//
-// Also, since it is common for only a specific output in a given utxo entry to
-// be referenced from a redeeming transaction, the script and amount for a given
-// output is not uncompressed until the first time it is accessed.  This
-// provides a mechanism to avoid the overhead of needlessly uncompressing all
-// outputs for a given utxo entry at the time of load.
-//
-// The struct is aligned for memory efficiency.
-type utxoOutput struct {
-	pkScript      []byte // The public key script for the output.
-	amount        int64  // The amount of the output.
-	scriptVersion uint16 // The script version
-	compressed    bool   // The public key script is compressed.
-	spent         bool   // Output is spent.
-}
+// utxoFlags defines additional information and state for a transaction output
+// in a utxo view.  The bit representation is:
+//   bit  0    - containing transaction is a coinbase
+//   bit  1    - transaction output has been spent
+//   bit  2    - transaction output has been modified since it was loaded
+//   bit  3    - containing transaction has an expiry
+//   bits 4-7	 - transaction type
+type utxoFlags uint8
+
+const (
+	// utxoFlagCoinBase indicates that a txout was contained in a coinbase tx.
+	utxoFlagCoinBase utxoFlags = 1 << iota
+
+	// utxoFlagSpent indicates that a txout is spent.
+	utxoFlagSpent
+
+	// utxoFlagModified indicates that a txout has been modified since it was
+	// loaded.
+	utxoFlagModified
+
+	// utxoFlagHasExpiry indicates that a txout was contained in a tx that
+	// included an expiry.
+	utxoFlagHasExpiry
+)
 
-// maybeDecompress decompresses the amount and public key script fields of the
-// utxo and marks it decompressed if needed.
-func (o *utxoOutput) maybeDecompress(compressionVersion uint32) {
-	// Nothing to do if it's not compressed.
-	if !o.compressed {
-		return
+const (
+	// utxoFlagTxTypeBitmask describes the bitmask that yields bits 4-7 from
+	// utxoFlags.
+	utxoFlagTxTypeBitmask = 0xf0
+
+	// utxoFlagTxTypeShift is the number of bits to shift utxoFlags to the right
+	// to yield the correct integer value after applying the bitmask with AND.
+	utxoFlagTxTypeShift = 4
+)
+
+// encodeUtxoFlags returns utxoFlags representing the passed parameters.
+func encodeUtxoFlags(coinbase bool, spent bool, modified bool, hasExpiry bool,
+	txType stake.TxType) utxoFlags {
+
+	packedFlags := utxoFlags(txType) << utxoFlagTxTypeShift
+	if coinbase {
+		packedFlags |= utxoFlagCoinBase
+	}
+	if spent {
+		packedFlags |= utxoFlagSpent
+	}
+	if modified {
+		packedFlags |= utxoFlagModified
 	}
+	if hasExpiry {
+		packedFlags |= utxoFlagHasExpiry
+	}
+
+	return packedFlags
+}
 
-	o.pkScript = decompressScript(o.pkScript, compressionVersion)
-	o.compressed = false
+// ticketMinimalOutputs stores the minimal outputs for ticket transactions and
+// is used in ticket submission utxo entries.
+//
+// The minimal outputs for ticket transactions are stored since all outputs of a
+// ticket need to be retrieved when validating vote transaction inputs or
+// revoking a ticket.
+type ticketMinimalOutputs struct {
+	data []byte
 }
 
-// UtxoEntry contains contextual information about an unspent transaction such
-// as whether or not it is a coinbase transaction, which block it was found in,
-// and the spent status of its outputs.
+// UtxoEntry houses details about an individual transaction output in a utxo
+// view such as whether or not it was contained in a coinbase tx, the height of
+// the block that contains the tx, whether or not it is spent, its public key
+// script, and how much it pays.
 //
 // The struct is aligned for memory efficiency.
 type UtxoEntry struct {
-	sparseOutputs map[uint32]*utxoOutput // Sparse map of unspent outputs.
-	stakeExtra    []byte                 // Extra data for the staking system.
+	amount   int64
+	pkScript []byte
 
-	txType    stake.TxType // The stake type of the transaction.
-	height    uint32       // Height of block containing tx.
-	index     uint32       // Index of containing tx in block.
-	txVersion uint16       // The tx version of this tx.
+	// ticketMinOuts is the minimal outputs for the ticket transaction that the
+	// output is contained in.  This is only stored in ticket submission outputs
+	// and is nil for all other output types.
+	//
+	// Note that this is using a pointer rather than a slice in order to occupy
+	// less space when it is nil.  It is nil in the vast majority of entries, so
+	// this provides a significant overall reduction in memory usage.
+	ticketMinOuts *ticketMinimalOutputs
+
+	blockHeight   uint32
+	blockIndex    uint32
+	scriptVersion uint16
+
+	// packedFlags contains additional info about the output as defined by
+	// utxoFlags.  This approach is used in order to reduce memory usage since
+	// there will be a lot of these in memory.
+	packedFlags utxoFlags
+}
 
-	isCoinBase bool // Whether entry is a coinbase tx.
-	hasExpiry  bool // Whether entry has an expiry.
-	modified   bool // Entry changed since load.
+// isModified returns whether or not the output has been modified since it was
+// loaded.
+func (entry *UtxoEntry) isModified() bool {
+	return entry.packedFlags&utxoFlagModified == utxoFlagModified
 }
 
-// TxVersion returns the version of the transaction the utxo represents.
-func (entry *UtxoEntry) TxVersion() uint16 {
-	return entry.txVersion
+// IsCoinBase returns whether or not the output was contained in a coinbase
+// transaction.
+func (entry *UtxoEntry) IsCoinBase() bool {
+	return entry.packedFlags&utxoFlagCoinBase == utxoFlagCoinBase
 }
 
-// HasExpiry returns the transaction expiry for the transaction that the utxo
-// entry represents.
-func (entry *UtxoEntry) HasExpiry() bool {
-	return entry.hasExpiry
+// IsSpent returns whether or not the output has been spent based upon the
+// current state of the unspent transaction output view it was obtained from.
+func (entry *UtxoEntry) IsSpent() bool {
+	return entry.packedFlags&utxoFlagSpent == utxoFlagSpent
 }
 
-// IsCoinBase returns whether or not the transaction the utxo entry represents
-// is a coinbase.
-func (entry *UtxoEntry) IsCoinBase() bool {
-	return entry.isCoinBase
+// HasExpiry returns whether or not the output was contained in a transaction
+// that included an expiry.
+func (entry *UtxoEntry) HasExpiry() bool {
+	return entry.packedFlags&utxoFlagHasExpiry == utxoFlagHasExpiry
 }
 
-// BlockHeight returns the height of the block containing the transaction the
-// utxo entry represents.
+// BlockHeight returns the height of the block containing the output.
 func (entry *UtxoEntry) BlockHeight() int64 {
-	return int64(entry.height)
+	return int64(entry.blockHeight)
 }
 
-// BlockIndex returns the height of the block containing the transaction the
-// utxo entry represents.
+// BlockIndex returns the index of the transaction that the output is contained
+// in.
 func (entry *UtxoEntry) BlockIndex() uint32 {
-	return entry.index
+	return entry.blockIndex
 }
 
-// TransactionType returns the type of the transaction the utxo entry
-// represents.
+// TransactionType returns the type of the transaction that the output is
+// contained in.
 func (entry *UtxoEntry) TransactionType() stake.TxType {
-	return entry.txType
+	txType := (entry.packedFlags & utxoFlagTxTypeBitmask) >> utxoFlagTxTypeShift
+	return stake.TxType(txType)
 }
 
-// IsOutputSpent returns whether or not the provided output index has been
-// spent based upon the current state of the unspent transaction output view
-// the entry was obtained from.
-//
-// Returns true if the output index references an output that does not exist
-// either due to it being invalid or because the output is not part of the view
-// due to previously being spent/pruned.
-func (entry *UtxoEntry) IsOutputSpent(outputIndex uint32) bool {
-	output, ok := entry.sparseOutputs[outputIndex]
-	if !ok {
-		return true
-	}
-
-	return output.spent
-}
-
-// SpendOutput marks the output at the provided index as spent.  Specifying an
-// output index that does not exist will not have any effect.
-func (entry *UtxoEntry) SpendOutput(outputIndex uint32) {
-	output, ok := entry.sparseOutputs[outputIndex]
-	if !ok {
-		return
-	}
-
+// Spend marks the output as spent.  Spending an output that is already spent
+// has no effect.
+func (entry *UtxoEntry) Spend() {
 	// Nothing to do if the output is already spent.
-	if output.spent {
+	if entry.IsSpent() {
 		return
 	}
 
-	entry.modified = true
-	output.spent = true
+	// Mark the output as spent and modified.
+	entry.packedFlags |= utxoFlagSpent | utxoFlagModified
 }
 
-// IsFullySpent returns whether or not the transaction the utxo entry represents
-// is fully spent.
-func (entry *UtxoEntry) IsFullySpent() bool {
-	// The entry is not fully spent if any of the outputs are unspent.
-	for _, output := range entry.sparseOutputs {
-		if !output.spent {
-			return false
-		}
-	}
-
-	return true
+// Amount returns the amount of the output.
+func (entry *UtxoEntry) Amount() int64 {
+	return entry.amount
 }
 
-// AmountByIndex returns the amount of the provided output index.
-//
-// Returns 0 if the output index references an output that does not exist
-// either due to it being invalid or because the output is not part of the view
-// due to previously being spent/pruned.
-func (entry *UtxoEntry) AmountByIndex(outputIndex uint32) int64 {
-	output, ok := entry.sparseOutputs[outputIndex]
-	if !ok {
-		return 0
-	}
-
-	return output.amount
+// PkScript returns the public key script for the output.
+func (entry *UtxoEntry) PkScript() []byte {
+	return entry.pkScript
 }
 
-// ScriptVersionByIndex returns the public key script for the provided output
-// index.
-//
-// Returns 0 if the output index references an output that does not exist
-// either due to it being invalid or because the output is not part of the view
-// due to previously being spent/pruned.
-func (entry *UtxoEntry) ScriptVersionByIndex(outputIndex uint32) uint16 {
-	output, ok := entry.sparseOutputs[outputIndex]
-	if !ok {
-		return 0
-	}
-
-	return output.scriptVersion
+// ScriptVersion returns the public key script version for the output.
+func (entry *UtxoEntry) ScriptVersion() uint16 {
+	return entry.scriptVersion
 }
 
-// PkScriptByIndex returns the public key script for the provided output index.
-//
-// Returns nil if the output index references an output that does not exist
-// either due to it being invalid or because the output is not part of the view
-// due to previously being spent/pruned.
-func (entry *UtxoEntry) PkScriptByIndex(outputIndex uint32) []byte {
-	output, ok := entry.sparseOutputs[outputIndex]
-	if !ok {
+// TicketMinimalOutputs returns the minimal outputs for the ticket transaction
+// that the output is contained in.  Note that the ticket minimal outputs are
+// only stored in ticket submission outputs and nil will be returned for all
+// other output types.
+func (entry *UtxoEntry) TicketMinimalOutputs() []*stake.MinimalOutput {
+	if entry.ticketMinOuts == nil {
 		return nil
 	}
 
-	// Ensure the output is decompressed before returning the script.
-	output.maybeDecompress(currentCompressionVersion)
-	return output.pkScript
+	minOuts, _ := deserializeToMinimalOutputs(entry.ticketMinOuts.data)
+	return minOuts
 }
 
-// Clone returns a deep copy of the utxo entry.
+// Clone returns a copy of the utxo entry.  It performs a deep copy for any
+// fields that are mutable.
 func (entry *UtxoEntry) Clone() *UtxoEntry {
 	if entry == nil {
 		return nil
 	}
 
 	newEntry := &UtxoEntry{
-		stakeExtra:    make([]byte, len(entry.stakeExtra)),
-		txVersion:     entry.txVersion,
-		height:        entry.height,
-		index:         entry.index,
-		txType:        entry.txType,
-		isCoinBase:    entry.isCoinBase,
-		hasExpiry:     entry.hasExpiry,
-		sparseOutputs: make(map[uint32]*utxoOutput),
-	}
-	copy(newEntry.stakeExtra, entry.stakeExtra)
-	for outputIndex, output := range entry.sparseOutputs {
-		newEntry.sparseOutputs[outputIndex] = &utxoOutput{
-			pkScript:      output.pkScript,
-			amount:        output.amount,
-			scriptVersion: output.scriptVersion,
-			compressed:    output.compressed,
-			spent:         output.spent,
-		}
+		amount:        entry.amount,
+		pkScript:      entry.pkScript,
+		ticketMinOuts: entry.ticketMinOuts,
+		blockHeight:   entry.blockHeight,
+		blockIndex:    entry.blockIndex,
+		scriptVersion: entry.scriptVersion,
+		packedFlags:   entry.packedFlags,
 	}
+
 	return newEntry
 }
 
-// newUtxoEntry returns a new unspent transaction output entry with the provided
-// coinbase flag and block height ready to have unspent outputs added.
-func newUtxoEntry(txVersion uint16, height uint32, index uint32, isCoinBase bool, hasExpiry bool, txType stake.TxType) *UtxoEntry {
-	return &UtxoEntry{
-		sparseOutputs: make(map[uint32]*utxoOutput),
-		txVersion:     txVersion,
-		height:        height,
-		index:         index,
-		isCoinBase:    isCoinBase,
-		hasExpiry:     hasExpiry,
-		txType:        txType,
-	}
+// isTicketSubmissionOutput returns true if the output is a ticket submission.
+func isTicketSubmissionOutput(txType stake.TxType, txOutIdx uint32) bool {
+	return txType == stake.TxTypeSStx && txOutIdx == 0
 }
 
 // UtxoViewpoint represents a view into the set of unspent transaction outputs
@@ -248,7 +227,7 @@ func newUtxoEntry(txVersion uint16, height uint32, index uint32, isCoinBase bool
 // The unspent outputs are needed by other transactions for things such as
 // script validation and double spend prevention.
 type UtxoViewpoint struct {
-	entries    map[chainhash.Hash]*UtxoEntry
+	entries    map[wire.OutPoint]*UtxoEntry
 	bestHash   chainhash.Hash
 	blockChain *BlockChain
 }
@@ -265,47 +244,85 @@ func (view *UtxoViewpoint) SetBestHash(hash *chainhash.Hash) {
 	view.bestHash = *hash
 }
 
-// LookupEntry returns information about a given transaction according to the
-// current state of the view.  It will return nil if the passed transaction
-// hash does not exist in the view or is otherwise not available such as when
-// it has been disconnected during a reorg.
-func (view *UtxoViewpoint) LookupEntry(txHash *chainhash.Hash) *UtxoEntry {
-	entry, ok := view.entries[*txHash]
-	if !ok {
-		return nil
-	}
-
-	return entry
+// LookupEntry returns information about a given transaction output according to
+// the current state of the view.  It will return nil if the passed output does
+// not exist in the view or is otherwise not available such as when it has been
+// disconnected during a reorg.
+func (view *UtxoViewpoint) LookupEntry(outpoint wire.OutPoint) *UtxoEntry {
+	return view.entries[outpoint]
 }
 
-// PrevScript returns the script and script version associated with the provided
-// previous outpoint along with a bool that indicates whether or not the
-// requested entry exists.  This ensures the caller is able to distinguish
-// between missing entry and empty v0 scripts.
-func (view *UtxoViewpoint) PrevScript(prevOut *wire.OutPoint) (uint16, []byte, bool) {
-	entry := view.LookupEntry(&prevOut.Hash)
+// addTxOut adds the specified output to the view if it is not provably
+// unspendable.  When the view already has an entry for the output, it will be
+// marked unspent.  All fields will be updated for existing entries since it's
+// possible it has changed during a reorg.
+func (view *UtxoViewpoint) addTxOut(outpoint wire.OutPoint, txOut *wire.TxOut,
+	packedFlags utxoFlags, blockHeight int64, blockIndex uint32,
+	ticketMinOuts *ticketMinimalOutputs) {
+
+	// Don't add provably unspendable outputs.
+	if txscript.IsUnspendable(txOut.Value, txOut.PkScript) {
+		return
+	}
+
+	// Update existing entries.  All fields are updated because it's possible
+	// (although extremely unlikely) that the existing entry is being replaced by
+	// a different transaction with the same hash.  This is allowed so long as the
+	// previous transaction is fully spent.
+	entry := view.LookupEntry(outpoint)
 	if entry == nil {
-		return 0, nil, false
+		entry = new(UtxoEntry)
+		view.entries[outpoint] = entry
 	}
 
-	version := entry.ScriptVersionByIndex(prevOut.Index)
-	pkScript := entry.PkScriptByIndex(prevOut.Index)
-	return version, pkScript, true
+	entry.amount = txOut.Value
+	entry.pkScript = txOut.PkScript
+	entry.blockHeight = uint32(blockHeight)
+	entry.blockIndex = blockIndex
+	entry.scriptVersion = txOut.Version
+	entry.packedFlags = packedFlags
+	entry.ticketMinOuts = ticketMinOuts
 }
 
-// PriorityInput returns the block height and amount associated with the
-// provided previous outpoint along with a bool that indicates whether or not
-// the requested entry exists.  This ensures the caller is able to distinguish
-// missing entries from zero values.
-func (view *UtxoViewpoint) PriorityInput(prevOut *wire.OutPoint) (int64, int64, bool) {
-	originHash := &prevOut.Hash
-	originIndex := prevOut.Index
-	txEntry := view.LookupEntry(originHash)
-	if txEntry != nil && !txEntry.IsOutputSpent(originIndex) {
-		return txEntry.BlockHeight(), txEntry.AmountByIndex(originIndex), true
+// AddTxOut adds the specified output of the passed transaction to the view if
+// it exists and is not provably unspendable.  When the view already has an
+// entry for the output, it will be marked unspent.  All fields will be updated
+// for existing entries since it's possible it has changed during a reorg.
+func (view *UtxoViewpoint) AddTxOut(tx *dcrutil.Tx, txOutIdx uint32,
+	blockHeight int64, blockIndex uint32, isTreasuryEnabled bool) {
+
+	// Can't add an output for an out of bounds index.
+	msgTx := tx.MsgTx()
+	if txOutIdx >= uint32(len(msgTx.TxOut)) {
+		return
 	}
 
-	return 0, 0, false
+	// Set encoded flags for the transaction.
+	isCoinBase := standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled)
+	const spent = false
+	hasExpiry := msgTx.Expiry != wire.NoExpiryValue
+	const modified = true
+	txType := stake.DetermineTxType(msgTx, isTreasuryEnabled)
+	tree := wire.TxTreeRegular
+	if txType != stake.TxTypeRegular {
+		tree = wire.TxTreeStake
+	}
+	flags := encodeUtxoFlags(isCoinBase, spent, modified, hasExpiry, txType)
+
+	// Update existing entries.  All fields are updated because it's possible
+	// (although extremely unlikely) that the existing entry is being replaced by
+	// a different transaction with the same hash.  This is allowed so long as the
+	// previous transaction is fully spent.
+	outpoint := wire.OutPoint{Hash: *tx.Hash(), Index: txOutIdx, Tree: tree}
+	txOut := msgTx.TxOut[txOutIdx]
+	var ticketMinOuts *ticketMinimalOutputs
+	if isTicketSubmissionOutput(txType, txOutIdx) {
+		ticketMinOuts = &ticketMinimalOutputs{
+			data: make([]byte, serializeSizeForMinimalOutputs(tx)),
+		}
+		putTxToMinimalOutputs(ticketMinOuts.data, tx)
+	}
+	view.addTxOut(outpoint, txOut, flags, blockHeight, blockIndex, ticketMinOuts)
 }
 
 // AddTxOuts adds all outputs in the passed transaction which are not provably
@@ -313,66 +330,77 @@ func (view *UtxoViewpoint) PriorityInput(prevOut *wire.OutPoint) (int64, int64,
 // outputs, they are simply marked unspent.  All fields will be updated for
 // existing entries since it's possible it has changed during a reorg.
 func (view *UtxoViewpoint) AddTxOuts(tx *dcrutil.Tx, blockHeight int64, blockIndex uint32, isTreasuryEnabled bool) {
-	// When there are not already any utxos associated with the transaction,
-	// add a new entry for it to the view.
-	entry := view.LookupEntry(tx.Hash())
-	if entry == nil {
-		msgTx := tx.MsgTx()
-		txType := stake.DetermineTxType(msgTx, isTreasuryEnabled)
-		entry = newUtxoEntry(msgTx.Version, uint32(blockHeight),
-			blockIndex, standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled),
-			msgTx.Expiry != 0, txType)
-		if txType == stake.TxTypeSStx {
-			stakeExtra := make([]byte, serializeSizeForMinimalOutputs(tx))
-			putTxToMinimalOutputs(stakeExtra, tx)
-			entry.stakeExtra = stakeExtra
-		}
-		view.entries[*tx.Hash()] = entry
-	} else {
-		entry.height = uint32(blockHeight)
-		entry.index = blockIndex
+	msgTx := tx.MsgTx()
+
+	// Set encoded flags for the transaction.
+	isCoinBase := standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled)
+	const spent = false
+	hasExpiry := msgTx.Expiry != wire.NoExpiryValue
+	const modified = true
+	txType := stake.DetermineTxType(msgTx, isTreasuryEnabled)
+	tree := wire.TxTreeRegular
+	if txType != stake.TxTypeRegular {
+		tree = wire.TxTreeStake
 	}
-	entry.modified = true
+	flags := encodeUtxoFlags(isCoinBase, spent, modified, hasExpiry, txType)
 
-	// Loop all of the transaction outputs and add those which are not
+	// Loop through all of the transaction outputs and add those which are not
 	// provably unspendable.
-	for txOutIdx, txOut := range tx.MsgTx().TxOut {
-		// TODO allow pruning of stake utxs after all other outputs are spent
-		if txscript.IsUnspendable(txOut.Value, txOut.PkScript) {
-			continue
+	outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
+	for txOutIdx, txOut := range msgTx.TxOut {
+		// Update existing entries.  All fields are updated because it's possible
+		// (although extremely unlikely) that the existing entry is being replaced
+		// by a different transaction with the same hash.  This is allowed so long
+		// as the previous transaction is fully spent.
+		outpoint.Index = uint32(txOutIdx)
+		var ticketMinOuts *ticketMinimalOutputs
+		if isTicketSubmissionOutput(txType, uint32(txOutIdx)) {
+			ticketMinOuts = &ticketMinimalOutputs{
+				data: make([]byte, serializeSizeForMinimalOutputs(tx)),
+			}
+			putTxToMinimalOutputs(ticketMinOuts.data, tx)
 		}
+		view.addTxOut(outpoint, txOut, flags, blockHeight, blockIndex,
+			ticketMinOuts)
+	}
+}
 
-		// Update existing entries.  All fields are updated because it's
-		// possible (although extremely unlikely) that the existing
-		// entry is being replaced by a different transaction with the
-		// same hash.  This is allowed so long as the previous
-		// transaction is fully spent.
-		if output, ok := entry.sparseOutputs[uint32(txOutIdx)]; ok {
-			output.spent = false
-			output.amount = txOut.Value
-			output.scriptVersion = txOut.Version
-			output.pkScript = txOut.PkScript
-			output.compressed = false
-			continue
-		}
+// PrevScript returns the script and script version associated with the provided
+// previous outpoint along with a bool that indicates whether or not the
+// requested entry exists.  This ensures the caller is able to distinguish
+// between missing entries and empty v0 scripts.
+func (view *UtxoViewpoint) PrevScript(prevOut *wire.OutPoint) (uint16, []byte, bool) {
+	entry := view.LookupEntry(*prevOut)
+	if entry == nil {
+		return 0, nil, false
+	}
 
-		// Add the unspent transaction output.
-		entry.sparseOutputs[uint32(txOutIdx)] = &utxoOutput{
-			spent:         false,
-			amount:        txOut.Value,
-			scriptVersion: txOut.Version,
-			pkScript:      txOut.PkScript,
-			compressed:    false,
-		}
+	version := entry.ScriptVersion()
+	pkScript := entry.PkScript()
+	return version, pkScript, true
+}
+
+// PriorityInput returns the block height and amount associated with the
+// provided previous outpoint along with a bool that indicates whether or not
+// the requested entry exists.  This ensures the caller is able to distinguish
+// missing entries from zero values.
+func (view *UtxoViewpoint) PriorityInput(prevOut *wire.OutPoint) (int64, int64, bool) {
+	entry := view.LookupEntry(*prevOut)
+	if entry != nil && !entry.IsSpent() {
+		return entry.BlockHeight(), entry.Amount(), true
 	}
+
+	return 0, 0, false
 }
 
 // connectTransaction updates the view by adding all new utxos created by the
-// passed transaction and marking all utxos that the transactions spend as
+// passed transaction and marking all utxos that the transaction spends as
 // spent.  In addition, when the 'stxos' argument is not nil, it will be updated
 // to append an entry for each spent txout.  An error will be returned if the
 // view does not contain the required utxos.
-func (view *UtxoViewpoint) connectTransaction(tx *dcrutil.Tx, blockHeight int64, blockIndex uint32, stxos *[]spentTxOut, isTreasuryEnabled bool) error {
+func (view *UtxoViewpoint) connectTransaction(tx *dcrutil.Tx, blockHeight int64,
+	blockIndex uint32, stxos *[]spentTxOut, isTreasuryEnabled bool) error {
+
 	// Coinbase transactions don't have any inputs to spend.
 	msgTx := tx.MsgTx()
 	if standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) {
@@ -386,9 +414,8 @@ func (view *UtxoViewpoint) connectTransaction(tx *dcrutil.Tx, blockHeight int64,
 		return nil
 	}
 
-	// Spend the referenced utxos by marking them spent in the view and,
-	// if a slice was provided for the spent txout details, append an entry
-	// to it.
+	// Spend the referenced utxos by marking them spent in the view and, if a
+	// slice was provided for the spent txout details, append an entry to it.
 	isVote := stake.IsSSGen(msgTx, isTreasuryEnabled)
 	var isTSpend bool
 	if isTreasuryEnabled {
@@ -405,47 +432,35 @@ func (view *UtxoViewpoint) connectTransaction(tx *dcrutil.Tx, blockHeight int64,
 			continue
 		}
 
-		// Ensure the referenced utxo exists in the view.  This should
-		// never happen unless there is a bug is introduced in the code.
-		originIndex := txIn.PreviousOutPoint.Index
-		entry := view.entries[txIn.PreviousOutPoint.Hash]
+		// Ensure the referenced utxo exists in the view.  This should never happen
+		// unless there is a bug is introduced in the code.
+		entry := view.entries[txIn.PreviousOutPoint]
 		if entry == nil {
 			return AssertError(fmt.Sprintf("view missing input %v",
 				txIn.PreviousOutPoint))
 		}
-		entry.SpendOutput(originIndex)
 
-		// Don't create the stxo details if not requested.
-		if stxos == nil {
-			continue
-		}
-
-		// Populate the stxo details using the utxo entry.  When the
-		// transaction is fully spent, set the additional stxo fields
-		// accordingly since those details will no longer be available
-		// in the utxo set.
-		var stxo = spentTxOut{
-			compressed:    false,
-			amount:        txIn.ValueIn,
-			scriptVersion: entry.ScriptVersionByIndex(originIndex),
-			pkScript:      entry.PkScriptByIndex(originIndex),
-		}
-		if entry.IsFullySpent() {
-			stxo.txVersion = entry.TxVersion()
-			stxo.height = uint32(entry.BlockHeight())
-			stxo.index = entry.BlockIndex()
-			stxo.isCoinBase = entry.IsCoinBase()
-			stxo.hasExpiry = entry.HasExpiry()
-			stxo.txType = entry.TransactionType()
-			stxo.txFullySpent = true
-
-			if entry.txType == stake.TxTypeSStx {
-				stxo.stakeExtra = entry.stakeExtra
+		// Only create the stxo details if requested.
+		if stxos != nil {
+			// Populate the stxo details using the utxo entry.
+			var stxo = spentTxOut{
+				amount:        entry.Amount(),
+				pkScript:      entry.PkScript(),
+				ticketMinOuts: entry.ticketMinOuts,
+				blockHeight:   uint32(entry.BlockHeight()),
+				blockIndex:    entry.BlockIndex(),
+				scriptVersion: entry.ScriptVersion(),
+				packedFlags: encodeFlags(entry.IsCoinBase(), entry.HasExpiry(),
+					entry.TransactionType()),
 			}
+
+			*stxos = append(*stxos, stxo)
 		}
 
-		// Append the entry to the provided spent txouts slice.
-		*stxos = append(*stxos, stxo)
+		// Mark the entry as spent.  This is not done until after the relevant
+		// details have been accessed since spending it might clear the fields from
+		// memory in the future.
+		entry.Spend()
 	}
 
 	// Add the transaction's outputs as available utxos.
@@ -454,15 +469,15 @@ func (view *UtxoViewpoint) connectTransaction(tx *dcrutil.Tx, blockHeight int64,
 	return nil
 }
 
-// disconnectTransactions updates the view by removing all utxos created by
-// the transactions in either the regular or stake tree of the block, depending
-// on the flag, and unspending all of the txos spent by those same transactions
-// by using the provided spent txo information.
+// disconnectTransactions updates the view by removing all utxos created by the
+// transactions in either the regular or stake tree of the block, depending on
+// the flag, and unspending all of the txos spent by those same transactions by
+// using the provided spent txo information.
 func (view *UtxoViewpoint) disconnectTransactions(block *dcrutil.Block, stxos []spentTxOut, stakeTree bool, isTreasuryEnabled bool) error {
 	// Choose which transaction tree to use and the appropriate offset into the
 	// spent transaction outputs that corresponds to them depending on the flag.
-	// Transactions in the stake tree are spent before transactions in the
-	// regular tree, thus skip all of the outputs spent by the regular tree when
+	// Transactions in the stake tree are spent before transactions in the regular
+	// tree, thus skip all of the outputs spent by the regular tree when
 	// disconnecting stake transactions.
 	stxoIdx := len(stxos) - 1
 	transactions := block.Transactions()
@@ -473,6 +488,7 @@ func (view *UtxoViewpoint) disconnectTransactions(block *dcrutil.Block, stxos []
 
 	for txIdx := len(transactions) - 1; txIdx > -1; txIdx-- {
 		tx := transactions[txIdx]
+		txHash := tx.Hash()
 		msgTx := tx.MsgTx()
 		txType := stake.TxTypeRegular
 		if stakeTree {
@@ -488,25 +504,60 @@ func (view *UtxoViewpoint) disconnectTransactions(block *dcrutil.Block, stxos []
 				stakeTree && txIdx == 0
 		}
 
-		// Clear this transaction from the view if it already exists or create a
-		// new empty entry for when it does not.  This is done because the code
-		// relies on its existence in the view in order to signal modifications
-		// have happened.
-		isCoinbase := !stakeTree && txIdx == 0
-		entry := view.entries[*tx.Hash()]
-		if entry == nil {
-			entry = newUtxoEntry(msgTx.Version, uint32(block.Height()),
-				uint32(txIdx), isCoinbase, msgTx.Expiry != 0, txType)
-			view.entries[*tx.Hash()] = entry
+		tree := wire.TxTreeRegular
+		if txType != stake.TxTypeRegular {
+			tree = wire.TxTreeStake
 		}
-		entry.modified = true
-		entry.sparseOutputs = make(map[uint32]*utxoOutput)
-
-		// Loop backwards through all of the transaction inputs (except
-		// for the coinbase and treasurybase which have no inputs) and
-		// unspend the referenced txos.  This is necessary to match the
-		// order of the spent txout entries.
-		if isCoinbase || isTreasuryBase || isTSpend {
+
+		isCoinBase := !stakeTree && txIdx == 0
+		hasExpiry := msgTx.Expiry != wire.NoExpiryValue
+
+		// It is instructive to note that there is no practical difference between
+		// a utxo that does not exist and one that has been spent with a pruned
+		// utxo set. So, even though the outputs here technically no longer exist,
+		// any missing entries are added to the view and marked spent because the
+		// code relies on their existence in the view in order to signal
+		// modifications have happened.
+		outpoint := wire.OutPoint{Hash: *txHash, Tree: tree}
+		for txOutIdx, txOut := range msgTx.TxOut {
+			// Don't add provably unspendable outputs.
+			if txscript.IsUnspendable(txOut.Value, txOut.PkScript) {
+				continue
+			}
+
+			outpoint.Index = uint32(txOutIdx)
+			entry := view.entries[outpoint]
+			if entry == nil {
+				const spent = false
+				const modified = true
+				entry = &UtxoEntry{
+					amount:        txOut.Value,
+					pkScript:      txOut.PkScript,
+					blockHeight:   uint32(block.Height()),
+					blockIndex:    uint32(txIdx),
+					scriptVersion: txOut.Version,
+					packedFlags: encodeUtxoFlags(isCoinBase, spent, modified, hasExpiry,
+						txType),
+				}
+
+				if isTicketSubmissionOutput(txType, uint32(txOutIdx)) {
+					entry.ticketMinOuts = &ticketMinimalOutputs{
+						data: make([]byte, serializeSizeForMinimalOutputs(tx)),
+					}
+					putTxToMinimalOutputs(entry.ticketMinOuts.data, tx)
+				}
+
+				view.entries[outpoint] = entry
+			}
+
+			entry.Spend()
+		}
+
+		// Loop backwards through all of the transaction inputs (except for the
+		// coinbase and treasurybase which have no inputs) and unspend the
+		// referenced txos.  This is necessary to match the order of the spent txout
+		// entries.
+		if isCoinBase || isTreasuryBase || isTSpend {
 			continue
 		}
 		for txInIdx := len(msgTx.TxIn) - 1; txInIdx > -1; txInIdx-- {
@@ -515,58 +566,50 @@ func (view *UtxoViewpoint) disconnectTransactions(block *dcrutil.Block, stxos []
 				continue
 			}
 
-			// Ensure the spent txout index is decremented to stay in sync with
-			// the transaction input.
+			// Ensure the spent txout index is decremented to stay in sync with the
+			// transaction input.
 			stxo := &stxos[stxoIdx]
 			stxoIdx--
 
-			// When there is not already an entry for the referenced transaction
-			// in the view, it means it was fully spent, so create a new utxo
-			// entry in order to resurrect it.
+			// When there is not already an entry for the referenced output in the
+			// view, it means it was previously spent, so create a new utxo entry in
+			// order to resurrect it.
 			txIn := msgTx.TxIn[txInIdx]
-			originHash := &txIn.PreviousOutPoint.Hash
-			originIndex := txIn.PreviousOutPoint.Index
-			entry := view.entries[*originHash]
+			entry := view.entries[txIn.PreviousOutPoint]
 			if entry == nil {
-				if !stxo.txFullySpent {
-					return AssertError(fmt.Sprintf("tried to revive unspent "+
-						"tx %v from non-fully spent stx entry", originHash))
-				}
-				entry = newUtxoEntry(msgTx.Version, stxo.height, stxo.index,
-					stxo.isCoinBase, stxo.hasExpiry, stxo.txType)
-				if stxo.txType == stake.TxTypeSStx {
-					entry.stakeExtra = stxo.stakeExtra
-				}
-				view.entries[*originHash] = entry
-			}
-
-			// Mark the entry as modified since it is either new or will be
-			// changed below.
-			entry.modified = true
-
-			// Restore the specific utxo using the stxo data from the spend
-			// journal if it doesn't already exist in the view.
-			output, ok := entry.sparseOutputs[originIndex]
-			if !ok {
-				// Add the unspent transaction output.
-				entry.sparseOutputs[originIndex] = &utxoOutput{
-					compressed:    stxo.compressed,
-					spent:         false,
+				const spent = false
+				const modified = true
+				entry = &UtxoEntry{
 					amount:        txIn.ValueIn,
-					scriptVersion: stxo.scriptVersion,
 					pkScript:      stxo.pkScript,
+					ticketMinOuts: stxo.ticketMinOuts,
+					blockHeight:   stxo.blockHeight,
+					blockIndex:    stxo.blockIndex,
+					scriptVersion: stxo.scriptVersion,
+					packedFlags: encodeUtxoFlags(stxo.IsCoinBase(), spent, modified,
+						stxo.HasExpiry(), stxo.TransactionType()),
 				}
-				continue
+
+				view.entries[txIn.PreviousOutPoint] = entry
 			}
 
-			// Mark the existing referenced transaction output as unspent.
-			output.spent = false
+			// Mark the existing referenced transaction output as unspent and
+			// modified.
+			entry.packedFlags &^= utxoFlagSpent
+			entry.packedFlags |= utxoFlagModified
 		}
 	}
 
 	return nil
 }
 
+// RemoveEntry removes the given transaction output from the current state of
+// the view.  It will have no effect if the passed output does not exist in the
+// view.
+func (view *UtxoViewpoint) RemoveEntry(outpoint wire.OutPoint) {
+	delete(view.entries, outpoint)
+}
+
 // disconnectRegularTransactions updates the view by removing all utxos created
 // by the transactions in regular tree of the provided block and unspending all
 // of the txos spent by those same transactions by using the provided spent txo
@@ -589,7 +632,6 @@ func (view *UtxoViewpoint) disconnectStakeTransactions(block *dcrutil.Block, stx
 // Disconnecting a transaction entails removing the utxos created by it and
 // restoring the outputs spent by it with the help of the provided spent txo
 // information.
-//func (view *UtxoViewpoint) disconnectDisapprovedBlock(db database.DB, block *dcrutil.Block, stxos []spentTxOut) error {
 func (view *UtxoViewpoint) disconnectDisapprovedBlock(db database.DB, block *dcrutil.Block, isTreasuryEnabled bool) error {
 	// Load all of the spent txos for the block from the database spend journal.
 	var stxos []spentTxOut
@@ -749,65 +791,63 @@ func (view *UtxoViewpoint) disconnectBlock(db database.DB, block, parent *dcruti
 }
 
 // Entries returns the underlying map that stores of all the utxo entries.
-func (view *UtxoViewpoint) Entries() map[chainhash.Hash]*UtxoEntry {
+func (view *UtxoViewpoint) Entries() map[wire.OutPoint]*UtxoEntry {
 	return view.entries
 }
 
-// commit prunes all entries marked modified that are now fully spent and marks
-// all entries as unmodified.
+// commit prunes all entries marked modified that are now spent and marks all
+// entries as unmodified.
 func (view *UtxoViewpoint) commit() {
-	for txHash, entry := range view.entries {
-		if entry == nil || (entry.modified && entry.IsFullySpent()) {
-			delete(view.entries, txHash)
+	for outpoint, entry := range view.entries {
+		if entry == nil || (entry.isModified() && entry.IsSpent()) {
+			delete(view.entries, outpoint)
 			continue
 		}
 
-		entry.modified = false
+		entry.packedFlags &^= utxoFlagModified
 	}
 }
 
 // viewFilteredSet represents a set of utxos to fetch from the database that are
 // not already in a view.
-type viewFilteredSet map[chainhash.Hash]struct{}
+type viewFilteredSet map[wire.OutPoint]struct{}
 
-// add conditionally adds the provided utxo hash to the set if it does not
+// add conditionally adds the provided outpoint to the set if it does not
 // already exist in the provided view.
-func (set viewFilteredSet) add(view *UtxoViewpoint, hash *chainhash.Hash) {
-	if _, ok := view.entries[*hash]; !ok {
-		set[*hash] = struct{}{}
+func (set viewFilteredSet) add(view *UtxoViewpoint, outpoint *wire.OutPoint) {
+	if _, ok := view.entries[*outpoint]; !ok {
+		set[*outpoint] = struct{}{}
 	}
 }
 
 // fetchUtxosMain fetches unspent transaction output data about the provided
-// set of transactions from the point of view of the end of the main chain at
-// the time of the call.
+// set of outpoints from the point of view of the main chain tip at the time of
+// the call.
 //
 // Upon completion of this function, the view will contain an entry for each
-// requested transaction.  Fully spent transactions, or those which otherwise
-// don't exist, will result in a nil entry in the view.
+// requested outpoint.  Spent outputs, or those which otherwise don't exist,
+// will result in a nil entry in the view.
 func (view *UtxoViewpoint) fetchUtxosMain(db database.DB, filteredSet viewFilteredSet) error {
-	// Nothing to do if there are no requested hashes.
+	// Nothing to do if there are no requested outputs.
 	if len(filteredSet) == 0 {
 		return nil
 	}
 
-	// Load the unspent transaction output information for the requested set
-	// of transactions from the point of view of the end of the main chain.
+	// Load the requested set of unspent transaction outputs from the point
+	// of view of the end of the main chain.
 	//
 	// NOTE: Missing entries are not considered an error here and instead
 	// will result in nil entries in the view.  This is intentionally done
-	// since other code uses the presence of an entry in the store as a way
-	// to optimize spend and unspend updates to apply only to the specific
-	// utxos that the caller needs access to.
+	// so other code can use the presence of an entry in the view as a way
+	// to unnecessarily avoid attempting to reload it from the database.
 	return db.View(func(dbTx database.Tx) error {
-		for hash := range filteredSet {
-			hashCopy := hash
-			entry, err := dbFetchUtxoEntry(dbTx, &hashCopy)
+		for outpoint := range filteredSet {
+			entry, err := dbFetchUtxoEntry(dbTx, outpoint)
 			if err != nil {
 				return err
 			}
 
-			view.entries[hash] = entry
+			view.entries[outpoint] = entry
 		}
 
 		return nil
@@ -857,7 +897,7 @@ func (view *UtxoViewpoint) addRegularInputUtxos(block *dcrutil.Block, isTreasury
 
 			// Only request entries that are not already in the view from the
 			// database.
-			filteredSet.add(view, originHash)
+			filteredSet.add(view, &txIn.PreviousOutPoint)
 		}
 	}
 
@@ -877,12 +917,12 @@ func (view *UtxoViewpoint) fetchRegularInputUtxos(db database.DB, block *dcrutil
 	return view.fetchUtxosMain(db, filteredSet)
 }
 
-// fetchInputUtxos loads utxo details about the input transactions referenced
-// by the transactions in both the regular and stake trees of the given block
-// into the view from the database as needed.  In the case of regular tree,
-// referenced entries that are earlier in the regular tree of the block are
-// added to the view.  In all cases, entries that are already in the view are
-// not modified.
+// fetchInputUtxos loads the unspent transaction outputs for the inputs
+// referenced by the transactions in both the regular and stake trees of the
+// given block into the view from the database as needed.  In the case of the
+// regular tree, referenced entries that are earlier in the regular tree of the
+// block are added to the view.  In all cases, entries that are already in the
+// view are not modified.
 func (view *UtxoViewpoint) fetchInputUtxos(db database.DB, block *dcrutil.Block, isTreasuryEnabled bool) error {
 	// Add any outputs of transactions in the regular tree of the block that are
 	// referenced by inputs of transactions that are located later in the tree
@@ -907,8 +947,7 @@ func (view *UtxoViewpoint) fetchInputUtxos(db database.DB, block *dcrutil.Block,
 
 			// Only request entries that are not already in the view
 			// from the database.
-			originHash := &txIn.PreviousOutPoint.Hash
-			filteredSet.add(view, originHash)
+			filteredSet.add(view, &txIn.PreviousOutPoint)
 		}
 	}
 
@@ -919,13 +958,13 @@ func (view *UtxoViewpoint) fetchInputUtxos(db database.DB, block *dcrutil.Block,
 // clone returns a deep copy of the view.
 func (view *UtxoViewpoint) clone() *UtxoViewpoint {
 	clonedView := &UtxoViewpoint{
-		entries:    make(map[chainhash.Hash]*UtxoEntry),
+		entries:    make(map[wire.OutPoint]*UtxoEntry),
 		bestHash:   view.bestHash,
 		blockChain: view.blockChain,
 	}
 
-	for txHash, entry := range view.entries {
-		clonedView.entries[txHash] = entry.Clone()
+	for outpoint, entry := range view.entries {
+		clonedView.entries[outpoint] = entry.Clone()
 	}
 
 	return clonedView
@@ -934,18 +973,17 @@ func (view *UtxoViewpoint) clone() *UtxoViewpoint {
 // NewUtxoViewpoint returns a new empty unspent transaction output view.
 func NewUtxoViewpoint(blockChain *BlockChain) *UtxoViewpoint {
 	return &UtxoViewpoint{
-		entries:    make(map[chainhash.Hash]*UtxoEntry),
+		entries:    make(map[wire.OutPoint]*UtxoEntry),
 		blockChain: blockChain,
 	}
 }
 
-// FetchUtxoView loads utxo details about the input transactions referenced by
-// the passed transaction from the point of view of the end of the main chain
-// while taking into account whether or not the transactions in the regular tree
-// of the current tip block should be included or not depending on the provided
-// flag.  It also attempts to fetch the utxo details for the transaction itself
-// so the returned view can be examined for duplicate unspent transaction
-// outputs.
+// FetchUtxoView loads unspent transaction outputs for the inputs referenced by
+// the passed transaction from the point of view of the main chain tip while
+// taking into account whether or not the transactions in the regular tree of
+// the current tip block should be included or not depending on the provided
+// flag.  It also attempts to fetch the utxos for the outputs of the transaction
+// itself so the returned view can be examined for duplicate transactions.
 //
 // This function is safe for concurrent access however the returned view is NOT.
 func (b *BlockChain) FetchUtxoView(tx *dcrutil.Tx, includeRegularTxns bool) (*UtxoViewpoint, error) {
@@ -1001,12 +1039,20 @@ func (b *BlockChain) FetchUtxoView(tx *dcrutil.Tx, includeRegularTxns bool) (*Ut
 	}
 
 	// Create a set of needed transactions based on those referenced by the
-	// inputs of the passed transaction.  Also, add the passed transaction
-	// itself as a way for the caller to detect duplicates that are not fully
-	// spent.
+	// inputs of the passed transaction.  Also, add the outputs of the transaction
+	// as a way for the caller to detect duplicates that are not fully spent.
 	filteredSet := make(viewFilteredSet)
-	filteredSet.add(view, tx.Hash())
 	msgTx := tx.MsgTx()
+	txType := stake.DetermineTxType(msgTx, isTreasuryEnabled)
+	tree := wire.TxTreeRegular
+	if txType != stake.TxTypeRegular {
+		tree = wire.TxTreeStake
+	}
+	outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
+	for txOutIdx := range msgTx.TxOut {
+		outpoint.Index = uint32(txOutIdx)
+		filteredSet.add(view, &outpoint)
+	}
 	if !standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) {
 		isVote := stake.IsSSGen(msgTx, isTreasuryEnabled)
 		for txInIdx, txIn := range msgTx.TxIn {
@@ -1015,7 +1061,7 @@ func (b *BlockChain) FetchUtxoView(tx *dcrutil.Tx, includeRegularTxns bool) (*Ut
 				continue
 			}
 
-			filteredSet.add(view, &txIn.PreviousOutPoint.Hash)
+			filteredSet.add(view, &txIn.PreviousOutPoint)
 		}
 	}
 
@@ -1023,24 +1069,24 @@ func (b *BlockChain) FetchUtxoView(tx *dcrutil.Tx, includeRegularTxns bool) (*Ut
 	return view, err
 }
 
-// FetchUtxoEntry loads and returns the unspent transaction output entry for the
-// passed hash from the point of view of the end of the main chain.
+// FetchUtxoEntry loads and returns the requested unspent transaction output
+// from the point of view of the the main chain tip.
 //
-// NOTE: Requesting a hash for which there is no data will NOT return an error.
-// Instead both the entry and the error will be nil.  This is done to allow
-// pruning of fully spent transactions.  In practice this means the caller must
-// check if the returned entry is nil before invoking methods on it.
+// NOTE: Requesting an output for which there is no data will NOT return an
+// error.  Instead both the entry and the error will be nil.  This is done to
+// allow pruning of spent transaction outputs.  In practice this means the
+// caller must check if the returned entry is nil before invoking methods on it.
 //
 // This function is safe for concurrent access however the returned entry (if
 // any) is NOT.
-func (b *BlockChain) FetchUtxoEntry(txHash *chainhash.Hash) (*UtxoEntry, error) {
+func (b *BlockChain) FetchUtxoEntry(outpoint wire.OutPoint) (*UtxoEntry, error) {
 	b.chainLock.RLock()
 	defer b.chainLock.RUnlock()
 
 	var entry *UtxoEntry
 	err := b.db.View(func(dbTx database.Tx) error {
 		var err error
-		entry, err = dbFetchUtxoEntry(dbTx, txHash)
+		entry, err = dbFetchUtxoEntry(dbTx, outpoint)
 		return err
 	})
 	if err != nil {
diff --git a/blockchain/validate.go b/blockchain/validate.go
index 47318dc7..4a9225bb 100644
--- a/blockchain/validate.go
+++ b/blockchain/validate.go
@@ -1839,31 +1839,39 @@ func (b *BlockChain) checkBlockContext(block *dcrutil.Block, prevNode *blockNode
 //
 // Decred: Check the stake transactions to make sure they don't have this txid
 // too.
-func (b *BlockChain) checkDupTxs(txSet []*dcrutil.Tx, view *UtxoViewpoint) error {
+func (b *BlockChain) checkDupTxs(txSet []*dcrutil.Tx, view *UtxoViewpoint, tree int8) error {
 	if !checkForDuplicateHashes {
 		return nil
 	}
 
-	// Fetch utxo details for all of the transactions in this block.
-	// Typically, there will not be any utxos for any of the transactions.
+	// Fetch utxos for all of the transaction outputs in this block.
+	// Typically, there will not be any utxos for any of the outputs.
 	filteredSet := make(viewFilteredSet)
 	for _, tx := range txSet {
-		filteredSet.add(view, tx.Hash())
+		outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
+		for txOutIdx := range tx.MsgTx().TxOut {
+			outpoint.Index = uint32(txOutIdx)
+			filteredSet.add(view, &outpoint)
+		}
 	}
 	err := view.fetchUtxosMain(b.db, filteredSet)
 	if err != nil {
 		return err
 	}
 
-	// Duplicate transactions are only allowed if the previous transaction
-	// is fully spent.
+	// Duplicate transaction outputs are only allowed if the previous
+	// transaction output is spent.
 	for _, tx := range txSet {
-		txEntry := view.LookupEntry(tx.Hash())
-		if txEntry != nil && !txEntry.IsFullySpent() {
-			str := fmt.Sprintf("tried to overwrite transaction %v "+
-				"at block height %d that is not fully spent",
-				tx.Hash(), txEntry.BlockHeight())
-			return ruleError(ErrOverwriteTx, str)
+		outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
+		for txOutIdx := range tx.MsgTx().TxOut {
+			outpoint.Index = uint32(txOutIdx)
+			entry := view.LookupEntry(outpoint)
+			if entry != nil && !entry.IsSpent() {
+				str := fmt.Sprintf("tried to overwrite transaction output %v "+
+					"at block height %d that is not spent",
+					outpoint, entry.BlockHeight())
+				return ruleError(ErrOverwriteTx, str)
+			}
 		}
 	}
 
@@ -1960,10 +1968,8 @@ func checkTicketPurchaseInputs(msgTx *wire.MsgTx, view *UtxoViewpoint) error {
 
 	for txInIdx := 0; txInIdx < len(msgTx.TxIn); txInIdx++ {
 		txIn := msgTx.TxIn[txInIdx]
-		originTxHash := &txIn.PreviousOutPoint.Hash
-		originTxIndex := txIn.PreviousOutPoint.Index
-		entry := view.LookupEntry(originTxHash)
-		if entry == nil || entry.IsOutputSpent(originTxIndex) {
+		entry := view.LookupEntry(txIn.PreviousOutPoint)
+		if entry == nil || entry.IsSpent() {
 			str := fmt.Sprintf("output %v referenced from transaction %s:%d "+
 				"either does not exist or has already been spent",
 				txIn.PreviousOutPoint, msgTx.TxHash(), txInIdx)
@@ -1974,14 +1980,14 @@ func checkTicketPurchaseInputs(msgTx *wire.MsgTx, view *UtxoViewpoint) error {
 		// particular, the allowed forms are pay-to-pubkey-hash and
 		// pay-to-script-hash either in the standard form (without a stake
 		// opcode) or their stake-tagged variant (with a stake opcode).
-		pkScriptVer := entry.ScriptVersionByIndex(originTxIndex)
+		pkScriptVer := entry.ScriptVersion()
 		if pkScriptVer != 0 {
 			str := fmt.Sprintf("output %v script version %d referenced by "+
 				"ticket %s:%d is not supported", txIn.PreviousOutPoint,
 				pkScriptVer, msgTx.TxHash(), txInIdx)
 			return ruleError(ErrTicketInputScript, str)
 		}
-		pkScript := entry.PkScriptByIndex(originTxIndex)
+		pkScript := entry.PkScript()
 		if !isAllowedTicketInputScriptForm(pkScript) {
 			str := fmt.Sprintf("output %v referenced from ticket %s:%d "+
 				"is not pay-to-pubkey-hash or pay-to-script-hash (script: %x)",
@@ -1995,7 +2001,7 @@ func checkTicketPurchaseInputs(msgTx *wire.MsgTx, view *UtxoViewpoint) error {
 		commitmentOutIdx := txInIdx*2 + 1
 		commitmentScript := msgTx.TxOut[commitmentOutIdx].PkScript
 		commitmentAmount := extractTicketCommitAmount(commitmentScript)
-		inputAmount := entry.AmountByIndex(originTxIndex)
+		inputAmount := entry.Amount()
 		change := msgTx.TxOut[commitmentOutIdx+1].Value
 		adjustedAmount := commitmentAmount + change
 		if adjustedAmount != inputAmount {
@@ -2104,12 +2110,12 @@ func extractTicketCommitFeeLimits(script []byte) uint16 {
 // Note that the returned error is not a RuleError, so the it is up to the
 // caller convert it accordingly.
 func checkTicketSubmissionInput(ticketUtxo *UtxoEntry) error {
-	submissionScriptVer := ticketUtxo.ScriptVersionByIndex(submissionOutputIdx)
+	submissionScriptVer := ticketUtxo.ScriptVersion()
 	if submissionScriptVer != 0 {
 		return fmt.Errorf("script version %d is not supported",
 			submissionScriptVer)
 	}
-	submissionScript := ticketUtxo.PkScriptByIndex(submissionOutputIdx)
+	submissionScript := ticketUtxo.PkScript()
 	if !isStakeSubmission(submissionScript) {
 		return fmt.Errorf("not a supported stake submission script (script: "+
 			"%x)", submissionScript)
@@ -2338,8 +2344,8 @@ func checkVoteInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx, txHe
 
 	// Ensure the referenced ticket is available.
 	ticketHash := &ticketIn.PreviousOutPoint.Hash
-	ticketUtxo := view.LookupEntry(ticketHash)
-	if ticketUtxo == nil || ticketUtxo.IsFullySpent() {
+	ticketUtxo := view.LookupEntry(ticketIn.PreviousOutPoint)
+	if ticketUtxo == nil || ticketUtxo.IsSpent() {
 		str := fmt.Sprintf("ticket output %v referenced by vote %s:%d either "+
 			"does not exist or has already been spent",
 			ticketIn.PreviousOutPoint, voteHash, ticketInIdx)
@@ -2373,7 +2379,7 @@ func checkVoteInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx, txHe
 		return ruleError(ErrImmatureTicketSpend, str)
 	}
 
-	ticketOuts := ConvertUtxosToMinimalOutputs(ticketUtxo)
+	ticketOuts := ticketUtxo.TicketMinimalOutputs()
 	if len(ticketOuts) == 0 {
 		panicf("missing extra stake data for ticket %v -- probable database "+
 			"corruption", ticketHash)
@@ -2438,8 +2444,8 @@ func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint,
 
 	// Ensure the referenced ticket is available.
 	ticketHash := &ticketIn.PreviousOutPoint.Hash
-	ticketUtxo := view.LookupEntry(ticketHash)
-	if ticketUtxo == nil || ticketUtxo.IsFullySpent() {
+	ticketUtxo := view.LookupEntry(ticketIn.PreviousOutPoint)
+	if ticketUtxo == nil || ticketUtxo.IsSpent() {
 		str := fmt.Sprintf("ticket output %v referenced from revocation %s:%d "+
 			"either does not exist or has already been spent",
 			ticketIn.PreviousOutPoint, revokeHash, ticketInIdx)
@@ -2475,7 +2481,7 @@ func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint,
 		return ruleError(ErrImmatureTicketSpend, str)
 	}
 
-	ticketOuts := ConvertUtxosToMinimalOutputs(ticketUtxo)
+	ticketOuts := ticketUtxo.TicketMinimalOutputs()
 	if len(ticketOuts) == 0 {
 		panicf("missing extra stake data for ticket %v -- probable database "+
 			"corruption", ticketHash)
@@ -2628,13 +2634,14 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T
 			continue
 		}
 
-		txInHash := &txIn.PreviousOutPoint.Hash
-		originTxIndex := txIn.PreviousOutPoint.Index
-		utxoEntry := view.LookupEntry(txInHash)
-		if utxoEntry == nil || utxoEntry.IsOutputSpent(originTxIndex) {
+		txInOutpoint := txIn.PreviousOutPoint
+		txInHash := &txInOutpoint.Hash
+		originTxIndex := txInOutpoint.Index
+		utxoEntry := view.LookupEntry(txInOutpoint)
+		if utxoEntry == nil || utxoEntry.IsSpent() {
 			str := fmt.Sprintf("output %v referenced from "+
 				"transaction %s:%d either does not exist or "+
-				"has already been spent", txIn.PreviousOutPoint,
+				"has already been spent", txInOutpoint,
 				txHash, idx)
 			return 0, ruleError(ErrMissingTxOut, str)
 		}
@@ -2642,7 +2649,7 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T
 		// Check fraud proof witness data.
 
 		// Using zero value outputs as inputs is banned.
-		if utxoEntry.AmountByIndex(originTxIndex) == 0 {
+		if utxoEntry.Amount() == 0 {
 			str := fmt.Sprintf("tried to spend zero value output "+
 				"from input %v, idx %v", txInHash,
 				originTxIndex)
@@ -2651,10 +2658,10 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T
 
 		if checkFraudProof {
 			if txIn.ValueIn !=
-				utxoEntry.AmountByIndex(originTxIndex) {
+				utxoEntry.Amount() {
 				str := fmt.Sprintf("bad fraud check value in "+
 					"(expected %v, given %v) for txIn %v",
-					utxoEntry.AmountByIndex(originTxIndex),
+					utxoEntry.Amount(),
 					txIn.ValueIn, idx)
 				return 0, ruleError(ErrFraudAmountIn, str)
 			}
@@ -2710,29 +2717,11 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T
 			}
 		}
 
-		// Ensure that the outpoint's tx tree makes sense.
-		originTxOPTree := txIn.PreviousOutPoint.Tree
-		originTxType := utxoEntry.TransactionType()
-		indicatedTree := wire.TxTreeRegular
-		if originTxType != stake.TxTypeRegular {
-			indicatedTree = wire.TxTreeStake
-		}
-		if indicatedTree != originTxOPTree {
-			errStr := fmt.Sprintf("tx %v attempted to spend from "+
-				"a %v tx tree (hash %v), yet the outpoint "+
-				"specified a %v tx tree instead", txHash,
-				indicatedTree, txIn.PreviousOutPoint.Hash,
-				originTxOPTree)
-			return 0, ruleError(ErrDiscordantTxTree, errStr)
-		}
-
 		// OP_TADD and OP_TGEN tagged outputs can only be spent after
 		// CoinbaseMaturity blocks.
 		if isTreasuryEnabled {
-			scriptClass := txscript.GetScriptClass(
-				utxoEntry.ScriptVersionByIndex(originTxIndex),
-				utxoEntry.PkScriptByIndex(originTxIndex),
-				isTreasuryEnabled)
+			scriptClass := txscript.GetScriptClass(utxoEntry.ScriptVersion(),
+				utxoEntry.PkScript(), isTreasuryEnabled)
 			if scriptClass == txscript.TreasuryAddTy ||
 				scriptClass == txscript.TreasuryGenTy {
 				originHeight := utxoEntry.BlockHeight()
@@ -2756,9 +2745,8 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T
 		// from non votes and revocations and make sure that they spend no
 		// OP_SSTX tagged outputs.
 		if !(isVote || isRevocation) {
-			if stake.IsTicketPurchaseScript(
-				utxoEntry.ScriptVersionByIndex(originTxIndex),
-				utxoEntry.PkScriptByIndex(originTxIndex)) {
+			if stake.IsTicketPurchaseScript(utxoEntry.ScriptVersion(),
+				utxoEntry.PkScript()) {
 				errSSGen := stake.CheckSSGen(msgTx,
 					isTreasuryEnabled)
 				errSSRtx := stake.CheckSSRtx(msgTx)
@@ -2774,12 +2762,10 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T
 
 		// OP_SSGEN and OP_SSRTX tagged outputs can only be spent after
 		// coinbase maturity many blocks.
-		if stake.IsRevocationScript(
-			utxoEntry.ScriptVersionByIndex(originTxIndex),
-			utxoEntry.PkScriptByIndex(originTxIndex)) ||
-			stake.IsVoteScript(
-				utxoEntry.ScriptVersionByIndex(originTxIndex),
-				utxoEntry.PkScriptByIndex(originTxIndex)) {
+		if stake.IsRevocationScript(utxoEntry.ScriptVersion(),
+			utxoEntry.PkScript()) ||
+			stake.IsVoteScript(utxoEntry.ScriptVersion(),
+				utxoEntry.PkScript()) {
 			originHeight := utxoEntry.BlockHeight()
 			blocksSincePrev := txHeight - originHeight
 			if blocksSincePrev < reqStakeOutMaturity {
@@ -2795,9 +2781,8 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T
 
 		// Ticket change outputs may only be spent after ticket change
 		// maturity many blocks.
-		if stake.IsStakeChangeScript(
-			utxoEntry.ScriptVersionByIndex(originTxIndex),
-			utxoEntry.PkScriptByIndex(originTxIndex)) {
+		if stake.IsStakeChangeScript(utxoEntry.ScriptVersion(),
+			utxoEntry.PkScript()) {
 			originHeight := utxoEntry.BlockHeight()
 			blocksSincePrev := txHeight - originHeight
 			if blocksSincePrev <
@@ -2817,7 +2802,7 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T
 		// in a transaction are in a unit value known as an atom.  One
 		// Decred is a quantity of atoms as defined by the AtomPerCoin
 		// constant.
-		originTxAtom := utxoEntry.AmountByIndex(originTxIndex)
+		originTxAtom := utxoEntry.Amount()
 		if originTxAtom < 0 {
 			str := fmt.Sprintf("transaction output has negative "+
 				"value of %v", originTxAtom)
@@ -2935,20 +2920,19 @@ func CountP2SHSigOps(tx *dcrutil.Tx, isCoinBaseTx bool, isStakeBaseTx bool, view
 	totalSigOps := 0
 	for txInIndex, txIn := range msgTx.TxIn {
 		// Ensure the referenced input transaction is available.
-		originTxHash := &txIn.PreviousOutPoint.Hash
-		originTxIndex := txIn.PreviousOutPoint.Index
-		utxoEntry := view.LookupEntry(originTxHash)
-		if utxoEntry == nil || utxoEntry.IsOutputSpent(originTxIndex) {
+		txInOutpoint := txIn.PreviousOutPoint
+		utxoEntry := view.LookupEntry(txInOutpoint)
+		if utxoEntry == nil || utxoEntry.IsSpent() {
 			str := fmt.Sprintf("output %v referenced from "+
 				"transaction %s:%d either does not exist or "+
-				"has already been spent", txIn.PreviousOutPoint,
+				"has already been spent", txInOutpoint,
 				tx.Hash(), txInIndex)
 			return 0, ruleError(ErrMissingTxOut, str)
 		}
 
 		// We're only interested in pay-to-script-hash types, so skip
 		// this input if it's not one.
-		pkScript := utxoEntry.PkScriptByIndex(originTxIndex)
+		pkScript := utxoEntry.PkScript()
 		if !txscript.IsPayToScriptHash(pkScript) {
 			continue
 		}
@@ -2966,7 +2950,7 @@ func CountP2SHSigOps(tx *dcrutil.Tx, isCoinBaseTx bool, isStakeBaseTx bool, view
 		if totalSigOps < lastSigOps {
 			str := fmt.Sprintf("the public key script from output "+
 				"%v contains too many signature operations - "+
-				"overflow", txIn.PreviousOutPoint)
+				"overflow", txInOutpoint)
 			return 0, ruleError(ErrTooManySigOps, str)
 		}
 	}
@@ -3021,16 +3005,16 @@ func checkStakeBaseAmounts(subsidyCache *standalone.SubsidyCache, height int64,
 		msgTx := tx.MsgTx()
 		if stake.IsSSGen(msgTx, isTreasuryEnabled) {
 			// Ensure the input is available.
-			txInHash := &msgTx.TxIn[1].PreviousOutPoint.Hash
-			utxoEntry, exists := view.entries[*txInHash]
+			txInOutpoint := msgTx.TxIn[1].PreviousOutPoint
+			txInHash := &txInOutpoint.Hash
+			utxoEntry, exists := view.entries[txInOutpoint]
 			if !exists || utxoEntry == nil {
 				str := fmt.Sprintf("couldn't find input tx %v "+
 					"for stakebase amounts check", txInHash)
 				return ruleError(ErrTicketUnavailable, str)
 			}
 
-			originTxIndex := msgTx.TxIn[1].PreviousOutPoint.Index
-			originTxAtom := utxoEntry.AmountByIndex(originTxIndex)
+			originTxAtom := utxoEntry.Amount()
 
 			totalOutputs := int64(0)
 			// Sum up the outputs.
@@ -3066,16 +3050,16 @@ func getStakeBaseAmounts(txs []*dcrutil.Tx, view *UtxoViewpoint, isTreasuryEnabl
 		msgTx := tx.MsgTx()
 		if stake.IsSSGen(msgTx, isTreasuryEnabled) {
 			// Ensure the input is available.
-			txInHash := &msgTx.TxIn[1].PreviousOutPoint.Hash
-			utxoEntry, exists := view.entries[*txInHash]
+			txInOutpoint := msgTx.TxIn[1].PreviousOutPoint
+			txInHash := &txInOutpoint.Hash
+			utxoEntry, exists := view.entries[txInOutpoint]
 			if !exists || utxoEntry == nil {
 				str := fmt.Sprintf("couldn't find input tx %v "+
 					"for stakebase amounts get", txInHash)
 				return 0, ruleError(ErrTicketUnavailable, str)
 			}
 
-			originTxIndex := msgTx.TxIn[1].PreviousOutPoint.Index
-			originTxAtom := utxoEntry.AmountByIndex(originTxIndex)
+			originTxAtom := utxoEntry.Amount()
 
 			totalInputs += originTxAtom
 
@@ -3119,8 +3103,9 @@ func getStakeTreeFees(subsidyCache *standalone.SubsidyCache, height int64, txs [
 				continue
 			}
 
-			txInHash := &in.PreviousOutPoint.Hash
-			utxoEntry, exists := view.entries[*txInHash]
+			txInOutpoint := in.PreviousOutPoint
+			txInHash := &txInOutpoint.Hash
+			utxoEntry, exists := view.entries[txInOutpoint]
 			if !exists || utxoEntry == nil {
 				str := fmt.Sprintf("couldn't find input tx "+
 					"%v for stake tree fee calculation",
@@ -3128,8 +3113,7 @@ func getStakeTreeFees(subsidyCache *standalone.SubsidyCache, height int64, txs [
 				return 0, ruleError(ErrTicketUnavailable, str)
 			}
 
-			originTxIndex := in.PreviousOutPoint.Index
-			originTxAtom := utxoEntry.AmountByIndex(originTxIndex)
+			originTxAtom := utxoEntry.Amount()
 
 			totalInputs += originTxAtom
 		}
@@ -3562,7 +3546,7 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.B
 
 	// Ensure the stake transaction tree does not contain any transactions
 	// that 'overwrite' older transactions which are not fully spent.
-	err = b.checkDupTxs(block.STransactions(), view)
+	err = b.checkDupTxs(block.STransactions(), view, wire.TxTreeStake)
 	if err != nil {
 		log.Tracef("checkDupTxs failed for cur TxTreeStake: %v", err)
 		return err
@@ -3635,7 +3619,7 @@ func (b *BlockChain) checkConnectBlock(node *blockNode, block, parent *dcrutil.B
 
 	// Ensure the regular transaction tree does not contain any transactions
 	// that 'overwrite' older transactions which are not fully spent.
-	err = b.checkDupTxs(block.Transactions(), view)
+	err = b.checkDupTxs(block.Transactions(), view, wire.TxTreeRegular)
 	if err != nil {
 		log.Tracef("checkDupTxs failed for cur TxTreeRegular: %v", err)
 		return err
diff --git a/gcs/blockcf2/blockcf.go b/gcs/blockcf2/blockcf.go
index 2ac61505..ac194fce 100644
--- a/gcs/blockcf2/blockcf.go
+++ b/gcs/blockcf2/blockcf.go
@@ -376,8 +376,8 @@ func Regular(block *wire.MsgBlock, prevScripts PrevScripter) (*gcs.FilterV2, err
 
 				// Commit to the change outputs with the exception of those that
 				// are provably unspendable.
-				isChangeOuput := txOutIdx%2 == 0
-				if isChangeOuput {
+				isChangeOutput := txOutIdx%2 == 0
+				if isChangeOutput {
 					if txOut.Value == 0 {
 						continue
 					}
diff --git a/internal/mempool/mempool.go b/internal/mempool/mempool.go
index 90a8a85a..c5473b20 100644
--- a/internal/mempool/mempool.go
+++ b/internal/mempool/mempool.go
@@ -425,10 +425,10 @@ func (mp *TxPool) removeOrphan(tx *dcrutil.Tx, removeRedeemers bool, isTreasuryE
 			tree = wire.TxTreeStake
 		}
 
-		prevOut := wire.OutPoint{Hash: *txHash, Tree: tree}
+		outpoint := wire.OutPoint{Hash: *txHash, Tree: tree}
 		for txOutIdx := range tx.MsgTx().TxOut {
-			prevOut.Index = uint32(txOutIdx)
-			for _, orphan := range mp.orphansByPrev[prevOut] {
+			outpoint.Index = uint32(txOutIdx)
+			for _, orphan := range mp.orphansByPrev[outpoint] {
 				mp.removeOrphan(orphan, true, isTreasuryEnabled)
 			}
 		}
@@ -698,10 +698,10 @@ func (mp *TxPool) fetchRedeemers(outpoints map[wire.OutPoint]*dcrutil.Tx, tx *dc
 	tree := wire.TxTreeRegular
 	seen := map[chainhash.Hash]struct{}{}
 	redeemers := make([]*dcrutil.Tx, 0)
-	prevOut := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
+	outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
 	for i := uint32(0); i < uint32(len(tx.MsgTx().TxOut)); i++ {
-		prevOut.Index = i
-		txRedeemer, exists := outpoints[prevOut]
+		outpoint.Index = i
+		txRedeemer, exists := outpoints[outpoint]
 		if !exists {
 			continue
 		}
@@ -838,16 +838,16 @@ func (mp *TxPool) removeTransaction(tx *dcrutil.Tx, removeRedeemers bool, isTrea
 			tree = wire.TxTreeStake
 		}
 
-		prevOut := wire.OutPoint{Hash: *txHash, Tree: tree}
+		outpoint := wire.OutPoint{Hash: *txHash, Tree: tree}
 		for i := uint32(0); i < uint32(len(tx.MsgTx().TxOut)); i++ {
-			prevOut.Index = i
-			if txRedeemer, exists := mp.outpoints[prevOut]; exists {
+			outpoint.Index = i
+			if txRedeemer, exists := mp.outpoints[outpoint]; exists {
 				mp.removeTransaction(txRedeemer, true,
 					isTreasuryEnabled)
 				continue
 			}
-			if txRedeemer, exists := mp.stagedOutpoints[prevOut]; exists {
-				log.Tracef("Removing staged transaction %v", prevOut.Hash)
+			if txRedeemer, exists := mp.stagedOutpoints[outpoint]; exists {
+				log.Tracef("Removing staged transaction %v", outpoint.Hash)
 				mp.removeStagedTransaction(txRedeemer)
 			}
 		}
@@ -1116,18 +1116,24 @@ func (mp *TxPool) fetchInputUtxos(tx *dcrutil.Tx, isTreasuryEnabled bool) (*bloc
 	}
 
 	// Attempt to populate any missing inputs from the transaction pool.
-	for originHash, entry := range utxoView.Entries() {
-		if entry != nil && !entry.IsFullySpent() {
+	for _, txIn := range tx.MsgTx().TxIn {
+		prevOut := &txIn.PreviousOutPoint
+		entry := utxoView.LookupEntry(*prevOut)
+		if entry != nil && !entry.IsSpent() {
 			continue
 		}
 
-		if poolTxDesc, exists := mp.pool[originHash]; exists {
-			utxoView.AddTxOuts(poolTxDesc.Tx, mining.UnminedHeight,
+		if poolTxDesc, exists := mp.pool[prevOut.Hash]; exists {
+			// AddTxOut ignores out of range index values, so it is safe to call without
+			// bounds checking here.
+			utxoView.AddTxOut(poolTxDesc.Tx, prevOut.Index, mining.UnminedHeight,
 				wire.NullBlockIndex, isTreasuryEnabled)
 		}
 
-		if stagedTx, exists := mp.staged[originHash]; exists {
-			utxoView.AddTxOuts(stagedTx, mining.UnminedHeight,
+		if stagedTx, exists := mp.staged[prevOut.Hash]; exists {
+			// AddTxOut ignores out of range index values, so it is safe to call without
+			// bounds checking here.
+			utxoView.AddTxOut(stagedTx, prevOut.Index, mining.UnminedHeight,
 				wire.NullBlockIndex, isTreasuryEnabled)
 		}
 	}
@@ -1225,11 +1231,11 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
 	// Then, be sure to set the tx tree correctly as it's possible a user submitted
 	// it to the network with TxTreeUnknown.
 	txType := stake.DetermineTxType(msgTx, isTreasuryEnabled)
-	if txType == stake.TxTypeRegular {
-		tx.SetTree(wire.TxTreeRegular)
-	} else {
-		tx.SetTree(wire.TxTreeStake)
+	tree := wire.TxTreeRegular
+	if txType != stake.TxTypeRegular {
+		tree = wire.TxTreeStake
 	}
+	tx.SetTree(tree)
 	isVote := txType == stake.TxTypeSSGen
 
 	var isTreasuryBase, isTSpend bool
@@ -1377,21 +1383,26 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
 
 	// Don't allow the transaction if it exists in the main chain and is not
 	// already fully spent.
-	txEntry := utxoView.LookupEntry(txHash)
-	if txEntry != nil && !txEntry.IsFullySpent() {
-		return nil, txRuleError(ErrAlreadyExists, "transaction already exists")
+	outpoint := wire.OutPoint{Hash: *txHash, Tree: tree}
+	for txOutIdx := range msgTx.TxOut {
+		outpoint.Index = uint32(txOutIdx)
+		entry := utxoView.LookupEntry(outpoint)
+		if entry != nil && !entry.IsSpent() {
+			return nil, txRuleError(ErrAlreadyExists, "transaction already exists")
+		}
+		utxoView.RemoveEntry(outpoint)
 	}
-	delete(utxoView.Entries(), *txHash)
 
-	// Transaction is an orphan if any of the inputs don't exist.
+	// Transaction is an orphan if any of the referenced transaction outputs don't
+	// exist or are already spent.
 	var missingParents []*chainhash.Hash
 	for i, txIn := range msgTx.TxIn {
 		if (i == 0 && (isVote || isTreasuryBase)) || isTSpend {
 			continue
 		}
 
-		entry := utxoView.LookupEntry(&txIn.PreviousOutPoint.Hash)
-		if entry == nil || entry.IsFullySpent() {
+		entry := utxoView.LookupEntry(txIn.PreviousOutPoint)
+		if entry == nil || entry.IsSpent() {
 			// Must make a copy of the hash here since the iterator
 			// is replaced and taking its address directly would
 			// result in all of the entries pointing to the same
@@ -1407,10 +1418,9 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow
 					txIn.PreviousOutPoint.Hash)
 				continue
 			}
-			if entry.IsFullySpent() {
-				log.Tracef("Transaction %v uses full spent input %v "+
-					"and will be considered an orphan", txHash,
-					txIn.PreviousOutPoint.Hash)
+			if entry.IsSpent() {
+				log.Tracef("Transaction %v uses spent input %v and will be considered "+
+					"an orphan", txHash, txIn.PreviousOutPoint.Hash)
 			}
 		}
 	}
@@ -1784,7 +1794,7 @@ func (mp *TxPool) processOrphans(acceptedTx *dcrutil.Tx, isTreasuryEnabled bool)
 			tree = wire.TxTreeStake
 		}
 
-		prevOut := wire.OutPoint{Hash: *processItem.Hash(), Tree: tree}
+		outpoint := wire.OutPoint{Hash: *processItem.Hash(), Tree: tree}
 		for txOutIdx := range processItem.MsgTx().TxOut {
 			// Look up all orphans that redeem the output that is
 			// now available.  This will typically only be one, but
@@ -1797,8 +1807,8 @@ func (mp *TxPool) processOrphans(acceptedTx *dcrutil.Tx, isTreasuryEnabled bool)
 			// would otherwise make outputs unspendable.
 			//
 			// Skip to the next available output if there are none.
-			prevOut.Index = uint32(txOutIdx)
-			orphans, exists := mp.orphansByPrev[prevOut]
+			outpoint.Index = uint32(txOutIdx)
+			orphans, exists := mp.orphansByPrev[outpoint]
 			if !exists {
 				continue
 			}
@@ -2181,11 +2191,11 @@ func New(cfg *Config) *TxPool {
 	// 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()}
+		outpoint := 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 {
+			outpoint.Index = i
+			if txRedeemer, exists := mp.outpoints[outpoint]; exists {
 				f(&mp.pool[txRedeemer.MsgTx().TxHash()].TxDesc)
 			}
 		}
diff --git a/internal/mempool/policy.go b/internal/mempool/policy.go
index c3a29ff0..3d5879f7 100644
--- a/internal/mempool/policy.go
+++ b/internal/mempool/policy.go
@@ -124,10 +124,9 @@ func checkInputsStandard(tx *dcrutil.Tx, txType stake.TxType, utxoView *blockcha
 		// It is safe to elide existence and index checks here since
 		// they have already been checked prior to calling this
 		// function.
-		prevOut := txIn.PreviousOutPoint
-		entry := utxoView.LookupEntry(&prevOut.Hash)
-		originPkScriptVer := entry.ScriptVersionByIndex(prevOut.Index)
-		originPkScript := entry.PkScriptByIndex(prevOut.Index)
+		entry := utxoView.LookupEntry(txIn.PreviousOutPoint)
+		originPkScriptVer := entry.ScriptVersion()
+		originPkScript := entry.PkScript()
 		switch txscript.GetScriptClass(originPkScriptVer,
 			originPkScript, isTreasuryEnabled) {
 		case txscript.ScriptHashTy:
diff --git a/internal/mining/mining.go b/internal/mining/mining.go
index 7d2c5e58..f9fb981d 100644
--- a/internal/mining/mining.go
+++ b/internal/mining/mining.go
@@ -398,13 +398,13 @@ type BlockTemplate struct {
 // mergeUtxoView adds all of the entries in viewB to viewA.  The result is that
 // viewA will contain all of its original entries plus all of the entries
 // in viewB.  It will replace any entries in viewB which also exist in viewA
-// if the entry in viewA is fully spent.
+// if the entry in viewA is spent.
 func mergeUtxoView(viewA *blockchain.UtxoViewpoint, viewB *blockchain.UtxoViewpoint) {
 	viewAEntries := viewA.Entries()
-	for hash, entryB := range viewB.Entries() {
-		if entryA, exists := viewAEntries[hash]; !exists ||
-			entryA == nil || entryA.IsFullySpent() {
-			viewAEntries[hash] = entryB
+	for outpoint, entryB := range viewB.Entries() {
+		if entryA, exists := viewAEntries[outpoint]; !exists ||
+			entryA == nil || entryA.IsSpent() {
+			viewAEntries[outpoint] = entryB
 		}
 	}
 }
@@ -665,11 +665,9 @@ func createTreasuryBaseTx(subsidyCache *standalone.SubsidyCache, nextBlockHeight
 // which are not provably unspendable as available unspent transaction outputs.
 func spendTransaction(utxoView *blockchain.UtxoViewpoint, tx *dcrutil.Tx, height int64, isTreasuryEnabled bool) {
 	for _, txIn := range tx.MsgTx().TxIn {
-		originHash := &txIn.PreviousOutPoint.Hash
-		originIndex := txIn.PreviousOutPoint.Index
-		entry := utxoView.LookupEntry(originHash)
+		entry := utxoView.LookupEntry(txIn.PreviousOutPoint)
 		if entry != nil {
-			entry.SpendOutput(originIndex)
+			entry.Spend()
 		}
 	}
 
@@ -749,16 +747,14 @@ func (g *BlkTmplGenerator) maybeInsertStakeTx(stx *dcrutil.Tx, treeValid bool, i
 			continue
 		}
 
-		originHash := &txIn.PreviousOutPoint.Hash
-		utxIn := view.LookupEntry(originHash)
-		if utxIn == nil {
+		entry := view.LookupEntry(txIn.PreviousOutPoint)
+		if entry == nil {
 			missingInput = true
 			break
 		} else {
-			originIdx := txIn.PreviousOutPoint.Index
-			txIn.ValueIn = utxIn.AmountByIndex(originIdx)
-			txIn.BlockHeight = uint32(utxIn.BlockHeight())
-			txIn.BlockIndex = utxIn.BlockIndex()
+			txIn.ValueIn = entry.Amount()
+			txIn.BlockHeight = uint32(entry.BlockHeight())
+			txIn.BlockIndex = entry.BlockIndex()
 		}
 	}
 	return !missingInput
@@ -1221,9 +1217,8 @@ mempoolLoop:
 			}
 
 			originHash := &txIn.PreviousOutPoint.Hash
-			originIndex := txIn.PreviousOutPoint.Index
-			utxoEntry := utxos.LookupEntry(originHash)
-			if utxoEntry == nil || utxoEntry.IsOutputSpent(originIndex) {
+			entry := utxos.LookupEntry(txIn.PreviousOutPoint)
+			if entry == nil || entry.IsSpent() {
 				if !g.cfg.TxSource.HaveTransaction(originHash) {
 					log.Tracef("Skipping tx %s because "+
 						"it references unspent output "+
@@ -1957,9 +1952,9 @@ nextPriorityQueueItem:
 			break
 		}
 
-		utxs, err := g.cfg.FetchUtxoView(tx, !knownDisapproved)
+		view, err := g.cfg.FetchUtxoView(tx, !knownDisapproved)
 		if err != nil {
-			str := fmt.Sprintf("failed to fetch input utxs for tx %v: %s",
+			str := fmt.Sprintf("failed to fetch utxo view for tx %v: %s",
 				tx.Hash(), err.Error())
 			return nil, makeError(ErrFetchTxStore, str)
 		}
@@ -1970,17 +1965,15 @@ nextPriorityQueueItem:
 		tx = txCopy
 
 		for _, txIn := range tx.MsgTx().TxIn {
-			originHash := &txIn.PreviousOutPoint.Hash
-			utx := utxs.LookupEntry(originHash)
-			if utx == nil {
+			entry := view.LookupEntry(txIn.PreviousOutPoint)
+			if entry == nil {
 				// Set a flag with the index so we can properly set
 				// the fraud proof below.
 				txIn.BlockIndex = wire.NullBlockIndex
 			} else {
-				originIdx := txIn.PreviousOutPoint.Index
-				txIn.ValueIn = utx.AmountByIndex(originIdx)
-				txIn.BlockHeight = uint32(utx.BlockHeight())
-				txIn.BlockIndex = utx.BlockIndex()
+				txIn.ValueIn = entry.Amount()
+				txIn.BlockHeight = uint32(entry.BlockHeight())
+				txIn.BlockIndex = entry.BlockIndex()
 			}
 		}
 	}
diff --git a/internal/netsync/manager.go b/internal/netsync/manager.go
index 675f926f..652e4e62 100644
--- a/internal/netsync/manager.go
+++ b/internal/netsync/manager.go
@@ -14,6 +14,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/decred/dcrd/blockchain/stake/v4"
 	"github.com/decred/dcrd/blockchain/v4"
 	"github.com/decred/dcrd/chaincfg/chainhash"
 	"github.com/decred/dcrd/chaincfg/v3"
@@ -110,10 +111,11 @@ type getSyncPeerMsg struct {
 // this through the sync manager so the sync manager doesn't ban the peer
 // when it sends this information back.
 type requestFromPeerMsg struct {
-	peer   *peerpkg.Peer
-	blocks []*chainhash.Hash
-	txs    []*chainhash.Hash
-	reply  chan requestFromPeerResponse
+	peer         *peerpkg.Peer
+	blocks       []*chainhash.Hash
+	voteHashes   []*chainhash.Hash
+	tSpendHashes []*chainhash.Hash
+	reply        chan requestFromPeerResponse
 }
 
 // requestFromPeerResponse is a response sent to the reply channel of a
@@ -1305,13 +1307,34 @@ func (m *SyncManager) haveInventory(invVect *wire.InvVect) (bool, error) {
 			return true, nil
 		}
 
-		// Check if the transaction exists from the point of view of the
-		// end of the main chain.
-		entry, err := m.cfg.Chain.FetchUtxoEntry(&invVect.Hash)
-		if err != nil {
-			return false, err
+		// Check if the transaction exists from the point of view of the main chain
+		// tip.  Note that this is only a best effort since it is expensive to check
+		// existence of every output and the only purpose of this check is to avoid
+		// downloading already known transactions.
+		//
+		// The first three outputs are checked because it covers the majority of
+		// common scenarios, including:
+		//   - The vast majority of regular transactions consist of two outputs
+		//     where one is some form of "pay-to-somebody-else" and the other is a
+		//     change output.
+		//   - For some stake transactions (e.g. votes) the first two outputs will
+		//     never be found as a utxo, since they are not spendable, so we need to
+		//     check through at least the third output.
+		outpoint := wire.OutPoint{Hash: invVect.Hash}
+		trees := []int8{wire.TxTreeRegular, wire.TxTreeStake}
+		for _, tree := range trees {
+			outpoint.Tree = tree
+			for i := uint32(0); i < 3; i++ {
+				outpoint.Index = i
+				entry, err := m.cfg.Chain.FetchUtxoEntry(outpoint)
+				if err != nil {
+					return false, err
+				}
+				if entry != nil && !entry.IsSpent() {
+					return true, nil
+				}
+			}
 		}
-		return entry != nil && !entry.IsFullySpent(), nil
 	}
 
 	// The requested inventory is an unsupported type, so just claim
@@ -1566,7 +1589,8 @@ out:
 				msg.reply <- peerID
 
 			case requestFromPeerMsg:
-				err := m.requestFromPeer(msg.peer, msg.blocks, msg.txs)
+				err := m.requestFromPeer(msg.peer, msg.blocks, msg.voteHashes,
+					msg.tSpendHashes)
 				msg.reply <- requestFromPeerResponse{
 					err: err,
 				}
@@ -1699,11 +1723,13 @@ func (m *SyncManager) SyncPeerID() int32 {
 // RequestFromPeer allows an outside caller to request blocks or transactions
 // from a peer.  The requests are logged in the internal map of requests so the
 // peer is not later banned for sending the respective data.
-func (m *SyncManager) RequestFromPeer(p *peerpkg.Peer, blocks, txs []*chainhash.Hash) error {
+func (m *SyncManager) RequestFromPeer(p *peerpkg.Peer, blocks, voteHashes,
+	tSpendHashes []*chainhash.Hash) error {
+
 	reply := make(chan requestFromPeerResponse, 1)
 	select {
-	case m.msgChan <- requestFromPeerMsg{peer: p, blocks: blocks, txs: txs,
-		reply: reply}:
+	case m.msgChan <- requestFromPeerMsg{peer: p, blocks: blocks,
+		voteHashes: voteHashes, tSpendHashes: tSpendHashes, reply: reply}:
 	case <-m.quit:
 	}
 
@@ -1715,7 +1741,9 @@ func (m *SyncManager) RequestFromPeer(p *peerpkg.Peer, blocks, txs []*chainhash.
 	}
 }
 
-func (m *SyncManager) requestFromPeer(p *peerpkg.Peer, blocks, txs []*chainhash.Hash) error {
+func (m *SyncManager) requestFromPeer(p *peerpkg.Peer, blocks, voteHashes,
+	tSpendHashes []*chainhash.Hash) error {
+
 	peer := lookupPeer(p, m.peers)
 	if peer == nil {
 		return fmt.Errorf("unknown peer %s", p)
@@ -1748,41 +1776,80 @@ func (m *SyncManager) requestFromPeer(p *peerpkg.Peer, blocks, txs []*chainhash.
 		m.requestedBlocks[*bh] = struct{}{}
 	}
 
-	// Add the vote transactions to the request.
-	for _, vh := range txs {
-		// If we've already requested this transaction, skip it.
-		_, alreadyReqP := peer.requestedTxns[*vh]
-		_, alreadyReqB := m.requestedTxns[*vh]
-
-		if alreadyReqP || alreadyReqB {
-			continue
+	addTxsToRequest := func(txs []*chainhash.Hash, txType stake.TxType) error {
+		// Return immediately if txs is nil.
+		if txs == nil {
+			return nil
 		}
 
-		// Ask the transaction memory pool if the transaction is known
-		// to it in any form (main pool or orphan).
-		if m.cfg.TxMemPool.HaveTransaction(vh) {
-			continue
-		}
+		for _, tx := range txs {
+			// If we've already requested this transaction, skip it.
+			_, alreadyReqP := peer.requestedTxns[*tx]
+			_, alreadyReqB := m.requestedTxns[*tx]
 
-		// Check if the transaction exists from the point of view of the
-		// end of the main chain.
-		entry, err := m.cfg.Chain.FetchUtxoEntry(vh)
-		if err != nil {
-			return err
-		}
-		if entry != nil {
-			continue
-		}
+			if alreadyReqP || alreadyReqB {
+				continue
+			}
 
-		err = msgResp.AddInvVect(wire.NewInvVect(wire.InvTypeTx, vh))
-		if err != nil {
-			return fmt.Errorf("unexpected error encountered building request "+
-				"for mining state vote %v: %v",
-				vh, err.Error())
+			// Ask the transaction memory pool if the transaction is known
+			// to it in any form (main pool or orphan).
+			if m.cfg.TxMemPool.HaveTransaction(tx) {
+				continue
+			}
+
+			// Check if the transaction exists from the point of view of the main
+			// chain tip.  Note that this is only a best effort since it is expensive
+			// to check existence of every output and the only purpose of this check
+			// is to avoid requesting already known transactions.
+			//
+			// Check for a specific outpoint based on the tx type.
+			outpoint := wire.OutPoint{Hash: *tx}
+			switch txType {
+			case stake.TxTypeSSGen:
+				// The first two outputs of vote transactions are OP_RETURN <data>, and
+				// therefore never exist as an unspent txo.  Use the third output, as
+				// the third output (and subsequent outputs) are OP_SSGEN outputs.
+				outpoint.Index = 2
+				outpoint.Tree = wire.TxTreeStake
+			case stake.TxTypeTSpend:
+				// The first output of a tSpend transaction is OP_RETURN <data>, and
+				// therefore never exists as an unspent txo.  Use the second output, as
+				// the second output (and subsequent outputs) are OP_TGEN outputs.
+				outpoint.Index = 1
+				outpoint.Tree = wire.TxTreeStake
+			}
+			entry, err := m.cfg.Chain.FetchUtxoEntry(outpoint)
+			if err != nil {
+				return err
+			}
+			if entry != nil {
+				continue
+			}
+
+			err = msgResp.AddInvVect(wire.NewInvVect(wire.InvTypeTx, tx))
+			if err != nil {
+				return fmt.Errorf("unexpected error encountered building request "+
+					"for mining state vote %v: %v",
+					tx, err.Error())
+			}
+
+			peer.requestedTxns[*tx] = struct{}{}
+			m.requestedTxns[*tx] = struct{}{}
 		}
 
-		peer.requestedTxns[*vh] = struct{}{}
-		m.requestedTxns[*vh] = struct{}{}
+		return nil
+	}
+
+	// Add the vote transactions to the request.
+	err := addTxsToRequest(voteHashes, stake.TxTypeSSGen)
+	if err != nil {
+		return err
+	}
+
+	// Add the tspend transactions to the request.
+	err = addTxsToRequest(tSpendHashes, stake.TxTypeTSpend)
+	if err != nil {
+		return err
 	}
 
 	if len(msgResp.InvList) > 0 {
diff --git a/internal/rpcserver/interface.go b/internal/rpcserver/interface.go
index 25960f6a..2fe00f8c 100644
--- a/internal/rpcserver/interface.go
+++ b/internal/rpcserver/interface.go
@@ -169,51 +169,35 @@ type UtxoEntry interface {
 	// ToUtxoEntry returns the underlying UtxoEntry instance.
 	ToUtxoEntry() *blockchain.UtxoEntry
 
-	// TransactionType returns the type of the transaction the utxo entry
-	// represents.
+	// TransactionType returns the type of the transaction that the output is
+	// contained in.
 	TransactionType() stake.TxType
 
-	// IsOutputSpent returns whether or not the provided output index has been
-	// spent based upon the current state of the unspent transaction output view
-	// the entry was obtained from.
-	//
-	// Returns true if the output index references an output that does not exist
-	// either due to it being invalid or because the output is not part of the view
-	// due to previously being spent/pruned.
-	IsOutputSpent(outputIndex uint32) bool
+	// IsSpent returns whether or not the output has been spent based upon the
+	// current state of the unspent transaction output view it was obtained from.
+	IsSpent() bool
 
-	// BlockHeight returns the height of the block containing the transaction the
-	// utxo entry represents.
+	// BlockHeight returns the height of the block containing the output.
 	BlockHeight() int64
 
-	// TxVersion returns the version of the transaction the utxo represents.
-	TxVersion() uint16
+	// Amount returns the amount of the output.
+	Amount() int64
 
-	// AmountByIndex returns the amount of the provided output index.
-	//
-	// Returns 0 if the output index references an output that does not exist
-	// either due to it being invalid or because the output is not part of the view
-	// due to previously being spent/pruned.
-	AmountByIndex(outputIndex uint32) int64
+	// ScriptVersion returns the public key script version for the output.
+	ScriptVersion() uint16
 
-	// ScriptVersionByIndex returns the public key script for the provided output
-	// index.
-	//
-	// Returns 0 if the output index references an output that does not exist
-	// either due to it being invalid or because the output is not part of the view
-	// due to previously being spent/pruned.
-	ScriptVersionByIndex(outputIndex uint32) uint16
+	// PkScript returns the public key script for the output.
+	PkScript() []byte
 
-	// PkScriptByIndex returns the public key script for the provided output index.
-	//
-	// Returns nil if the output index references an output that does not exist
-	// either due to it being invalid or because the output is not part of the view
-	// due to previously being spent/pruned.
-	PkScriptByIndex(outputIndex uint32) []byte
-
-	// IsCoinBase returns whether or not the transaction the utxo entry represents
-	// is a coinbase.
+	// IsCoinBase returns whether or not the output was contained in a coinbase
+	// transaction.
 	IsCoinBase() bool
+
+	// TicketMinimalOutputs returns the minimal outputs for the ticket transaction
+	// that the output is contained in.  Note that the ticket minimal outputs are
+	// only stored in ticket submission outputs and nil will be returned for all
+	// other output types.
+	TicketMinimalOutputs() []*stake.MinimalOutput
 }
 
 // Chain represents a chain for use with the RPC server.
@@ -272,11 +256,6 @@ type Chain interface {
 	// hash has been missed in the live ticket treap of the best node.
 	CheckMissedTickets(hashes []chainhash.Hash) []bool
 
-	// ConvertUtxosToMinimalOutputs converts the contents of a UTX to a series of
-	// minimal outputs. It does this so that these can be passed to stake subpackage
-	// functions, where they will be evaluated for correctness.
-	ConvertUtxosToMinimalOutputs(entry UtxoEntry) []*stake.MinimalOutput
-
 	// CountVoteVersion returns the total number of version votes for the current
 	// rule change activation interval.
 	CountVoteVersion(version uint32) (uint32, error)
@@ -288,17 +267,17 @@ type Chain interface {
 	// the interval.
 	EstimateNextStakeDifficulty(hash *chainhash.Hash, newTickets int64, useMaxTickets bool) (int64, error)
 
-	// FetchUtxoEntry loads and returns the unspent transaction output entry for the
-	// passed hash from the point of view of the end of the main chain.
+	// FetchUtxoEntry loads and returns the requested unspent transaction output
+	// from the point of view of the main chain tip.
 	//
-	// NOTE: Requesting a hash for which there is no data must NOT return an error.
-	// Instead both the entry and the error must be nil.  This is done to allow
-	// pruning of fully spent transactions.  In practice this means the caller must
-	// check if the returned entry is nil before invoking methods on it.
+	// NOTE: Requesting an output for which there is no data will NOT return an
+	// error.  Instead both the entry and the error will be nil.  This is done to
+	// allow pruning of spent transaction outputs.  In practice this means the
+	// caller must check if the returned entry is nil before invoking methods on it.
 	//
 	// This function is safe for concurrent access however the returned entry (if
 	// any) is NOT.
-	FetchUtxoEntry(txHash *chainhash.Hash) (UtxoEntry, error)
+	FetchUtxoEntry(outpoint wire.OutPoint) (UtxoEntry, error)
 
 	// FetchUtxoStats returns statistics on the current utxo set.
 	FetchUtxoStats() (*blockchain.UtxoStats, error)
diff --git a/internal/rpcserver/rpcserver.go b/internal/rpcserver/rpcserver.go
index 28cfae89..9dbd8eb2 100644
--- a/internal/rpcserver/rpcserver.go
+++ b/internal/rpcserver/rpcserver.go
@@ -966,17 +966,22 @@ func handleCreateRawSSRtx(_ context.Context, s *Server, cmd interface{}) (interf
 	}
 
 	// Try to fetch the ticket from the block database.
-	ticketUtx, err := s.cfg.Chain.FetchUtxoEntry(txHash)
-	if ticketUtx == nil || err != nil {
+	outpoint := wire.OutPoint{Hash: *txHash, Index: input.Vout, Tree: input.Tree}
+	ticketUtxo, err := s.cfg.Chain.FetchUtxoEntry(outpoint)
+	if ticketUtxo == nil || err != nil {
 		return nil, rpcNoTxInfoError(txHash)
 	}
-	if t := ticketUtx.TransactionType(); t != stake.TxTypeSStx {
+	if t := ticketUtxo.TransactionType(); t != stake.TxTypeSStx {
 		return nil, rpcDeserializationError("Invalid Tx type: %v", t)
 	}
 
 	// Store the sstx pubkeyhashes and amounts as found in the transaction
 	// outputs.
-	minimalOutputs := s.cfg.Chain.ConvertUtxosToMinimalOutputs(ticketUtx)
+	minimalOutputs := ticketUtxo.TicketMinimalOutputs()
+	if minimalOutputs == nil {
+		return nil, rpcInternalError("Missing ticket minimal outputs", "")
+	}
+
 	ssrtxPayTypes, ssrtxPkhs, sstxAmts, _, _, _ :=
 		stake.SStxStakeOutputInfo(minimalOutputs)
 
@@ -3440,7 +3445,6 @@ func handleGetTxOut(_ context.Context, s *Server, cmd interface{}) (interface{},
 	var pkScript []byte
 	var isCoinbase bool
 	var isTreasuryEnabled bool
-	var txTree int8
 	includeMempool := true
 	if c.IncludeMempool != nil {
 		includeMempool = *c.IncludeMempool
@@ -3448,6 +3452,14 @@ func handleGetTxOut(_ context.Context, s *Server, cmd interface{}) (interface{},
 	var txFromMempool *dcrutil.Tx
 	if includeMempool {
 		txFromMempool, _ = s.cfg.TxMempooler.FetchTransaction(txHash)
+
+		// Set as nil if the tx tree does not match the tree param that was passed.
+		// This is set as nil rather than returning immediately here since it is
+		// technically possible (though extremely unlikely) that the tx exists
+		// elsewhere, so it should still continue and check elsewhere below.
+		if txFromMempool != nil && txFromMempool.Tree() != c.Tree {
+			txFromMempool = nil
+		}
 	}
 	if txFromMempool != nil {
 		mtx := txFromMempool.MsgTx()
@@ -3480,9 +3492,9 @@ func handleGetTxOut(_ context.Context, s *Server, cmd interface{}) (interface{},
 		scriptVersion = txOut.Version
 		pkScript = txOut.PkScript
 		isCoinbase = standalone.IsCoinBaseTx(mtx, isTreasuryEnabled)
-		txTree = txFromMempool.Tree()
 	} else {
-		entry, err := chain.FetchUtxoEntry(txHash)
+		outpoint := wire.OutPoint{Hash: *txHash, Index: c.Vout, Tree: c.Tree}
+		entry, err := chain.FetchUtxoEntry(outpoint)
 		if err != nil {
 			context := "Failed to retrieve utxo entry"
 			return nil, rpcInternalError(err.Error(), context)
@@ -3494,7 +3506,7 @@ func handleGetTxOut(_ context.Context, s *Server, cmd interface{}) (interface{},
 		// transaction already in the main chain.  Mined transactions
 		// that are spent by a mempool transaction are not affected by
 		// this.
-		if entry == nil || entry.IsOutputSpent(c.Vout) {
+		if entry == nil || entry.IsSpent() {
 			return nil, nil
 		}
 
@@ -3513,19 +3525,10 @@ func handleGetTxOut(_ context.Context, s *Server, cmd interface{}) (interface{},
 
 		bestBlockHash = best.Hash.String()
 		confirmations = 1 + best.Height - entry.BlockHeight()
-		value = entry.AmountByIndex(c.Vout)
-		scriptVersion = entry.ScriptVersionByIndex(c.Vout)
-		pkScript = entry.PkScriptByIndex(c.Vout)
+		value = entry.Amount()
+		scriptVersion = entry.ScriptVersion()
+		pkScript = entry.PkScript()
 		isCoinbase = entry.IsCoinBase()
-		txTree = wire.TxTreeRegular
-		if entry.TransactionType() != stake.TxTypeRegular {
-			txTree = wire.TxTreeStake
-		}
-	}
-
-	// Return nil if the tx tree does not match the tree param that was passed.
-	if txTree != c.Tree {
-		return nil, nil
 	}
 
 	// Disassemble script into single line printable format.  The
diff --git a/rpcadaptors.go b/rpcadaptors.go
index 795d2ee9..3f89c618 100644
--- a/rpcadaptors.go
+++ b/rpcadaptors.go
@@ -11,7 +11,6 @@ import (
 	"net"
 	"time"
 
-	"github.com/decred/dcrd/blockchain/stake/v4"
 	"github.com/decred/dcrd/blockchain/v4"
 	"github.com/decred/dcrd/chaincfg/chainhash"
 	"github.com/decred/dcrd/chaincfg/v3"
@@ -379,25 +378,18 @@ type rpcChain struct {
 // Ensure rpcChain implements the rpcserver.Chain interface.
 var _ rpcserver.Chain = (*rpcChain)(nil)
 
-// ConvertUtxosToMinimalOutputs converts the contents of a UTX to a series of
-// minimal outputs. It does this so that these can be passed to stake subpackage
-// functions, where they will be evaluated for correctness.
-func (c *rpcChain) ConvertUtxosToMinimalOutputs(entry rpcserver.UtxoEntry) []*stake.MinimalOutput {
-	return blockchain.ConvertUtxosToMinimalOutputs(entry.ToUtxoEntry())
-}
-
-// FetchUtxoEntry loads and returns the unspent transaction output entry for the
-// passed hash from the point of view of the end of the main chain.
+// FetchUtxoEntry loads and returns the requested unspent transaction output
+// from the point of view of the main chain tip.
 //
-// NOTE: Requesting a hash for which there is no data will NOT return an error.
-// Instead both the entry and the error will be nil.  This is done to allow
-// pruning of fully spent transactions.  In practice this means the caller must
-// check if the returned entry is nil before invoking methods on it.
+// NOTE: Requesting an output for which there is no data will NOT return an
+// error.  Instead both the entry and the error will be nil.  This is done to
+// allow pruning of spent transaction outputs.  In practice this means the
+// caller must check if the returned entry is nil before invoking methods on it.
 //
 // This function is safe for concurrent access however the returned entry (if
 // any) is NOT.
-func (c *rpcChain) FetchUtxoEntry(txHash *chainhash.Hash) (rpcserver.UtxoEntry, error) {
-	utxo, err := c.BlockChain.FetchUtxoEntry(txHash)
+func (c *rpcChain) FetchUtxoEntry(outpoint wire.OutPoint) (rpcserver.UtxoEntry, error) {
+	utxo, err := c.BlockChain.FetchUtxoEntry(outpoint)
 	if utxo == nil || err != nil {
 		return nil, err
 	}
diff --git a/server.go b/server.go
index 2f072cd2..6afd502a 100644
--- a/server.go
+++ b/server.go
@@ -855,7 +855,7 @@ func (sp *serverPeer) OnGetMiningState(p *peer.Peer, msg *wire.MsgGetMiningState
 // requests the data advertised in the message from the peer.
 func (sp *serverPeer) OnMiningState(p *peer.Peer, msg *wire.MsgMiningState) {
 	err := sp.server.syncManager.RequestFromPeer(sp.Peer, msg.BlockHashes,
-		msg.VoteHashes)
+		msg.VoteHashes, nil)
 	if err != nil {
 		peerLog.Warnf("couldn't handle mining state message: %v",
 			err.Error())
@@ -947,20 +947,21 @@ func (sp *serverPeer) OnGetInitState(p *peer.Peer, msg *wire.MsgGetInitState) {
 // requests the data advertised in the message from the peer.
 func (sp *serverPeer) OnInitState(p *peer.Peer, msg *wire.MsgInitState) {
 	blockHashes := make([]*chainhash.Hash, 0, len(msg.BlockHashes))
-	txHashes := make([]*chainhash.Hash, 0, len(msg.VoteHashes)+len(msg.TSpendHashes))
+	voteHashes := make([]*chainhash.Hash, 0, len(msg.VoteHashes))
+	tSpendHashes := make([]*chainhash.Hash, 0, len(msg.TSpendHashes))
 
 	for i := range msg.BlockHashes {
 		blockHashes = append(blockHashes, &msg.BlockHashes[i])
 	}
 	for i := range msg.VoteHashes {
-		txHashes = append(txHashes, &msg.VoteHashes[i])
+		voteHashes = append(voteHashes, &msg.VoteHashes[i])
 	}
 	for i := range msg.TSpendHashes {
-		txHashes = append(txHashes, &msg.TSpendHashes[i])
+		tSpendHashes = append(tSpendHashes, &msg.TSpendHashes[i])
 	}
 
 	err := sp.server.syncManager.RequestFromPeer(sp.Peer, blockHashes,
-		txHashes)
+		voteHashes, tSpendHashes)
 	if err != nil {
 		peerLog.Warnf("couldn't handle init state message: %v", err)
 	}


Test patch:
diff --git a/blockchain/chainio_test.go b/blockchain/chainio_test.go
index 0ff57e60..c83a66e6 100644
--- a/blockchain/chainio_test.go
+++ b/blockchain/chainio_test.go
@@ -344,56 +344,63 @@ func TestBlockIndexDecodeErrors(t *testing.T) {
 func TestStxoSerialization(t *testing.T) {
 	t.Parallel()
 
+	// Define constants for indicating flags.
+	const (
+		noCoinbase   = false
+		withCoinbase = true
+		noExpiry     = false
+		withExpiry   = true
+	)
+
 	tests := []struct {
 		name       string
 		stxo       spentTxOut
-		txVersion  int32 // When the txout is not fully spent.
 		serialized []byte
-	}{
-		{
-			name: "Spends last output of coinbase",
-			stxo: spentTxOut{
-				amount:        9999,
-				scriptVersion: 0,
-				pkScript:      hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"),
-				height:        12345,
-				index:         54321,
-				isCoinBase:    true,
-				hasExpiry:     false,
-				txFullySpent:  true,
-				txType:        0,
-				txVersion:     1,
-			},
-			serialized: hexToBytes("4100006edbc6c4d31bae9f1ccc38538a114bf42de65e8601"),
-		},
-		{
-			name: "Spends last output of non coinbase and is a ticket",
-			stxo: spentTxOut{
-				amount:        9999,
-				scriptVersion: 0,
-				pkScript:      hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"),
-				height:        12345,
-				index:         54321,
-				isCoinBase:    false,
-				hasExpiry:     false,
-				txFullySpent:  true,
-				txType:        1,
-				txVersion:     1,
-				stakeExtra:    []byte{0x00},
-			},
-			serialized: hexToBytes("4400006edbc6c4d31bae9f1ccc38538a114bf42de65e860100"),
+		txOutIndex uint32
+	}{{
+		name: "Coinbase, no expiry",
+		stxo: spentTxOut{
+			amount:        9999,
+			scriptVersion: 0,
+			pkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688a" +
+				"c"),
+			blockHeight: 12345,
+			blockIndex:  54321,
+			packedFlags: encodeFlags(
+				withCoinbase,
+				noExpiry,
+				stake.TxTypeRegular,
+			),
 		},
-		{
-			name: "Does not spend last output",
-			stxo: spentTxOut{
-				amount:        34405000000,
-				pkScript:      hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"),
-				scriptVersion: 0,
+		serialized: hexToBytes("0100006edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
+		txOutIndex: 3,
+	}, {
+		name: "Ticket submission",
+		stxo: spentTxOut{
+			amount:        4294959555,
+			scriptVersion: 0,
+			pkScript: hexToBytes("ba76a914a13afb81d54c9f8bb0c5e082d56fd563ab9b35968" +
+				"8ac"),
+			ticketMinOuts: &ticketMinimalOutputs{
+				data: hexToBytes("03808efefade57001aba76a914a13afb81d54c9f8bb0c5e082d" +
+					"56fd563ab9b359688ac0000206a1e9ac39159847e259c9162405b5f6c8135d2c7e" +
+					"af1a375040001000000005800001abd76a91400000000000000000000000000000" +
+					"0000000000088ac"),
 			},
-			txVersion:  1,
-			serialized: hexToBytes("0000006edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
+			blockHeight: 85314,
+			blockIndex:  6,
+			packedFlags: encodeFlags(
+				noCoinbase,
+				withExpiry,
+				stake.TxTypeSStx,
+			),
 		},
-	}
+		serialized: hexToBytes("06005aba76a914a13afb81d54c9f8bb0c5e082d56fd563ab" +
+			"9b359688ac03808efefade57001aba76a914a13afb81d54c9f8bb0c5e082d56fd563a" +
+			"b9b359688ac0000206a1e9ac39159847e259c9162405b5f6c8135d2c7eaf1a3750400" +
+			"01000000005800001abd76a914000000000000000000000000000000000000000088ac"),
+		txOutIndex: 0,
+	}}
 
 	for _, test := range tests {
 		// Ensure the function to calculate the serialized size without
@@ -426,16 +433,19 @@ func TestStxoSerialization(t *testing.T) {
 		// stxo.
 		var gotStxo spentTxOut
 		offset, err := decodeSpentTxOut(test.serialized, &gotStxo,
-			test.stxo.amount, test.stxo.height, test.stxo.index)
+			test.stxo.amount, test.stxo.blockHeight, test.stxo.blockIndex,
+			test.txOutIndex)
 		if err != nil {
-			t.Errorf("decodeSpentTxOut (%s): unexpected error: %v",
-				test.name, err)
+			t.Errorf("decodeSpentTxOut (%s): unexpected error: %v", test.name, err)
 			continue
 		}
+		if !reflect.DeepEqual(gotStxo, test.stxo) {
+			t.Errorf("decodeSpentTxOut (%s):\nwant: %+v\n got: %+v\n", test.name,
+				test.stxo, gotStxo)
+		}
 		if offset != len(test.serialized) {
-			t.Errorf("decodeSpentTxOut (%s): did not get expected "+
-				"number of bytes read - got %d, want %d",
-				test.name, offset, len(test.serialized))
+			t.Errorf("decodeSpentTxOut (%s): did not get expected number of bytes "+
+				"read - got %d, want %d", test.name, offset, len(test.serialized))
 			continue
 		}
 	}
@@ -449,88 +459,91 @@ func TestStxoDecodeErrors(t *testing.T) {
 	tests := []struct {
 		name       string
 		stxo       spentTxOut
+		txOutIndex uint32
 		serialized []byte
 		errType    error
 		bytesRead  int // Expected number of bytes read.
-	}{
-		{
-			// [EOF]
-			name:       "nothing serialized (no flags)",
-			stxo:       spentTxOut{},
-			serialized: hexToBytes(""),
-			errType:    errDeserialize(""),
-			bytesRead:  0,
-		},
-		{
-			// [<flags 00> EOF]
-			name:       "no compressed txout script version",
-			stxo:       spentTxOut{},
-			serialized: hexToBytes("00"),
-			errType:    errDeserialize(""),
-			bytesRead:  1,
-		},
-		{
-			// [<flags 10> <script version 00> EOF]
-			name:       "no tx version data after empty script for a fully spent regular stxo",
-			stxo:       spentTxOut{},
-			serialized: hexToBytes("4000"),
-			errType:    errDeserialize(""),
-			bytesRead:  2,
-		},
-		{
-			// [<flags 10> <script version 00> <compressed pk script 01 6e ...> EOF]
-			name:       "no tx version data after a pay-to-script-hash script for a fully spent regular stxo",
-			stxo:       spentTxOut{},
-			serialized: hexToBytes("4000016edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
-			errType:    errDeserialize(""),
-			bytesRead:  23,
-		},
-		{
-			// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> EOF]
-			name:       "no stakeextra data after script for a fully spent ticket stxo",
-			stxo:       spentTxOut{},
-			serialized: hexToBytes("4400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601"),
-			errType:    errDeserialize(""),
-			bytesRead:  24,
-		},
-		{
-			// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01}> EOF]
-			name:       "truncated stakeextra data after script for a fully spent ticket stxo (num outputs only)",
-			stxo:       spentTxOut{},
-			serialized: hexToBytes("4400016edbc6c4d31bae9f1ccc38538a114bf42de65e860101"),
-			errType:    errDeserialize(""),
-			bytesRead:  25,
-		},
-		{
-			// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01} {amount 0f}> EOF]
-			name:       "truncated stakeextra data after script for a fully spent ticket stxo (num outputs and amount only)",
-			stxo:       spentTxOut{},
-			serialized: hexToBytes("4400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601010f"),
-			errType:    errDeserialize(""),
-			bytesRead:  26,
-		},
-		{
-			// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01} {amount 0f} {script version 00}> EOF]
-			name:       "truncated stakeextra data after script for a fully spent ticket stxo (num outputs, amount, and script version only)",
-			stxo:       spentTxOut{},
-			serialized: hexToBytes("4400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601010f00"),
-			errType:    errDeserialize(""),
-			bytesRead:  27,
-		},
-		{
-			// [<flags 14> <script version 00> <compressed pk script 01 6e ...> <tx version 01> <stakeextra {num outputs 01} {amount 0f} {script version 00} {script size 1a} {25 bytes of script instead of 26}> EOF]
-			name:       "truncated stakeextra data after script for a fully spent ticket stxo (script size specified as 0x1a, but only 0x19 bytes provided)",
-			stxo:       spentTxOut{},
-			serialized: hexToBytes("4400016edbc6c4d31bae9f1ccc38538a114bf42de65e8601010f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588"),
-			errType:    errDeserialize(""),
-			bytesRead:  28,
-		},
-	}
+	}{{
+		// [EOF]
+		name:       "nothing serialized (no flags)",
+		stxo:       spentTxOut{},
+		txOutIndex: 0,
+		serialized: hexToBytes(""),
+		errType:    errDeserialize(""),
+		bytesRead:  0,
+	}, {
+		// [<flags 01> EOF]
+		name:       "no data after flags",
+		stxo:       spentTxOut{},
+		txOutIndex: 0,
+		serialized: hexToBytes("01"),
+		errType:    errDeserialize(""),
+		bytesRead:  1,
+	}, {
+		// [<flags 01> <script version 00> <compressed pk script 12> EOF]
+		name:       "incomplete compressed txout",
+		stxo:       spentTxOut{},
+		txOutIndex: 0,
+		serialized: hexToBytes("010012"),
+		errType:    errDeserialize(""),
+		bytesRead:  2,
+	}, {
+		// [<flags 06> <script version 00> <compressed pk script 01 6e ...> EOF]
+		name: "no minimal output data after script for a ticket submission " +
+			"output",
+		stxo:       spentTxOut{},
+		txOutIndex: 0,
+		serialized: hexToBytes("0600016edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
+		errType:    errDeserialize(""),
+		bytesRead:  23,
+	}, {
+		// [<flags 06> <script version 00> <compressed pk script 01 6e ...>
+		//  <ticket min outs {num outputs 01}> EOF]
+		name: "truncated minimal output data after script for a ticket " +
+			"submission output (num outputs only)",
+		stxo:       spentTxOut{},
+		serialized: hexToBytes("0600016edbc6c4d31bae9f1ccc38538a114bf42de65e8601"),
+		errType:    errDeserialize(""),
+		bytesRead:  24,
+	}, {
+		// [<flags 06> <script version 00> <compressed pk script 01 6e ...>
+		//  <ticket min outs {num outputs 01} {amount 0f}> EOF]
+		name: "truncated minimal output data after script for a ticket " +
+			"submission output (num outputs and amount only)",
+		stxo: spentTxOut{},
+		serialized: hexToBytes("0600016edbc6c4d31bae9f1ccc38538a114bf42de65e86010" +
+			"f"),
+		errType:   errDeserialize(""),
+		bytesRead: 25,
+	}, {
+		// [<flags 06> <script version 00> <compressed pk script 01 6e ...>
+		//  <ticket min outs {num outputs 01} {amount 0f} {script version 00}> EOF]
+		name: "truncated minimal output data after script for a ticket " +
+			"submission output (num outputs, amount, and script version only)",
+		stxo: spentTxOut{},
+		serialized: hexToBytes("0600016edbc6c4d31bae9f1ccc38538a114bf42de65e86010" +
+			"f00"),
+		errType:   errDeserialize(""),
+		bytesRead: 26,
+	}, {
+		// [<flags 06> <script version 00> <compressed pk script 01 6e ...>
+		//  <ticket min outs {num outputs 01} {amount 0f} {script version 00}
+		//  {script size 1a} {25 bytes of script instead of 26}> EOF]
+		name: "truncated minimal output data after script for a ticket " +
+			"submission output (script size specified as 0x1a, but only 0x19 bytes " +
+			"provided)",
+		stxo: spentTxOut{},
+		serialized: hexToBytes("0600016edbc6c4d31bae9f1ccc38538a114bf42de65e86010" +
+			"f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588"),
+		errType:   errDeserialize(""),
+		bytesRead: 27,
+	}}
 
 	for _, test := range tests {
 		// Ensure the expected error type is returned.
 		gotBytesRead, err := decodeSpentTxOut(test.serialized,
-			&test.stxo, test.stxo.amount, test.stxo.height, test.stxo.index)
+			&test.stxo, test.stxo.amount, test.stxo.blockHeight, test.stxo.blockIndex,
+			test.txOutIndex)
 		if !errors.As(err, &test.errType) {
 			t.Errorf("decodeSpentTxOut (%s): expected error type "+
 				"does not match - got %T, want %T", test.name,
@@ -553,231 +566,149 @@ func TestStxoDecodeErrors(t *testing.T) {
 func TestSpendJournalSerialization(t *testing.T) {
 	t.Parallel()
 
+	// Define constants for indicating flags.
+	const (
+		noCoinbase   = false
+		withCoinbase = true
+		noExpiry     = false
+		withExpiry   = true
+	)
+
 	tests := []struct {
 		name       string
 		entry      []spentTxOut
 		blockTxns  []*wire.MsgTx
-		utxoView   *UtxoViewpoint
 		serialized []byte
-	}{
-		{
-			name:       "No spends",
-			entry:      nil,
-			blockTxns:  nil,
-			utxoView:   NewUtxoViewpoint(nil),
-			serialized: nil,
-		},
-		{
-			name: "One tx with one input spends last output of coinbase",
-			entry: []spentTxOut{{
-				amount:        5000000000,
-				scriptVersion: 0,
-				pkScript:      hexToBytes("0511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"),
-				compressed:    true,
-				height:        9,
-				index:         0,
-				isCoinBase:    true,
-				hasExpiry:     false,
-				txFullySpent:  true,
-				txType:        0,
-				txVersion:     1,
-				stakeExtra:    nil,
-			}},
-			blockTxns: []*wire.MsgTx{{ // Coinbase omitted.
-				SerType: wire.TxSerializeFull,
-				Version: 1,
-				TxIn: []*wire.TxIn{{
-					PreviousOutPoint: wire.OutPoint{
-						Hash:  *mustParseHash("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"),
-						Index: 0,
-						Tree:  0,
-					},
-					SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"),
-					Sequence:        0xffffffff,
-					BlockHeight:     9,
-					BlockIndex:      0,
-					ValueIn:         5000000000,
-				}},
-				TxOut: []*wire.TxOut{{
-					Value:    1000000000,
-					Version:  0,
-					PkScript: hexToBytes("4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac"),
-				}, {
-					Value:    4000000000,
-					Version:  0,
-					PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"),
-				}},
-				LockTime: 0,
-				Expiry:   0,
+	}{{
+		name:       "No spends",
+		entry:      nil,
+		blockTxns:  nil,
+		serialized: nil,
+	}, {
+		// Adapted from mainnet block 100267:
+		//   Regular tx that spends from a coinbase:
+		//     575a1f489c1d2135efcfc03f1556151fe4601d5bed235767e0c61cdda2b56757
+		//   Vote tx:
+		//     b3e36d0ee300d16c5a7fdc6de403c1dcf7434fba84ddcaf2e26b276c80051b98
+		//   (other transactions within that block omitted)
+		name: "One regular tx that spends from a coinbase and one vote tx",
+		entry: []spentTxOut{{
+			amount:        1597192852,
+			scriptVersion: 0,
+			pkScript: hexToBytes("76a914b3c2069c496bc13228c154b03993809f278233" +
+				"d188ac"),
+			blockHeight: 100000,
+			blockIndex:  0,
+			packedFlags: encodeFlags(
+				withCoinbase,
+				noExpiry,
+				stake.TxTypeRegular,
+			),
+		}, {
+			amount:        4294959555,
+			scriptVersion: 0,
+			pkScript: hexToBytes("ba76a914a13afb81d54c9f8bb0c5e082d56fd563ab9b" +
+				"359688ac"),
+			ticketMinOuts: &ticketMinimalOutputs{
+				data: hexToBytes("03808efefade57001aba76a914a13afb81d54c9f8bb0c5e082d" +
+					"56fd563ab9b359688ac0000206a1e9ac39159847e259c9162405b5f6c8135d2c7e" +
+					"af1a375040001000000005800001abd76a91400000000000000000000000000000" +
+					"0000000000088ac"),
+			},
+			blockHeight: 85314,
+			blockIndex:  6,
+			packedFlags: encodeFlags(
+				noCoinbase,
+				withExpiry,
+				stake.TxTypeSStx,
+			),
+		}},
+		blockTxns: []*wire.MsgTx{{
+			SerType: wire.TxSerializeFull,
+			Version: 1,
+			TxIn: []*wire.TxIn{{
+				PreviousOutPoint: wire.OutPoint{
+					Hash: *mustParseHash("0e0c8ac0b57b7bff8461e3c9e251ee05b1d04ee5ef971" +
+						"4b6414ea4bff1939fb9"),
+					Index: 2,
+					Tree:  0,
+				},
+				SignatureScript: hexToBytes("483045022100b7c226f487d4f3086c6f3b27a3f8" +
+					"6c15078ee3fc40effee28e88ad5bda81c36a02204ad34bb78ff170c3eaa383a12a" +
+					"d159c6104744a0bfd13130476a85240297a6fe012103da2b1c13507c9b96b39952" +
+					"0c3233f6c81eb4beed2d30be8077ad3a04854e4fec"),
+				Sequence:    0xffffffff,
+				BlockHeight: 100000,
+				BlockIndex:  0,
+				ValueIn:     1597192852,
 			}},
-			utxoView:   NewUtxoViewpoint(nil),
-			serialized: hexToBytes("41000511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c01"),
-		},
-		{
-			name: "Two txns when one spends last output, one doesn't",
-			entry: []spentTxOut{{
-				amount:        34405000000,
-				height:        321,
-				index:         123,
-				scriptVersion: 0,
-				pkScript:      hexToBytes("016edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
-				compressed:    true,
+			TxOut: []*wire.TxOut{{
+				Value:   1388604152,
+				Version: 0,
+				PkScript: hexToBytes("76a9142bf10bc24646c590d16b35b925cf7ad3c5aef0b28" +
+					"8ac"),
 			}, {
-				amount:        13761000000,
-				scriptVersion: 0,
-				pkScript:      hexToBytes("01b2fb57eadf61e106a100a7445a8c3f67898841ec"),
-				compressed:    true,
-				height:        3214,
-				index:         1234,
-				isCoinBase:    false,
-				hasExpiry:     false,
-				txFullySpent:  true,
-				txType:        0,
-				txVersion:     1,
-				stakeExtra:    nil,
+				Value:   208335700,
+				Version: 0,
+				PkScript: hexToBytes("76a914cf71aca9293855190e270650c405395ed00dfca58" +
+					"8ac"),
 			}},
-			blockTxns: []*wire.MsgTx{{ // Coinbase omitted.
-				SerType: wire.TxSerializeFull,
-				Version: 1,
-				TxIn: []*wire.TxIn{{
-					PreviousOutPoint: wire.OutPoint{
-						Hash:  *mustParseHash("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"),
-						Index: 1,
-						Tree:  0,
-					},
-					SignatureScript: hexToBytes("493046022100c167eead9840da4a033c9a56470d7794a9bb1605b377ebe5688499b39f94be59022100fb6345cab4324f9ea0b9ee9169337534834638d818129778370f7d378ee4a325014104d962cac5390f12ddb7539507065d0def320d68c040f2e73337c3a1aaaab7195cb5c4d02e0959624d534f3c10c3cf3d73ca5065ebd62ae986b04c6d090d32627c"),
-					Sequence:        0xffffffff,
-					BlockHeight:     321,
-					BlockIndex:      123,
-					ValueIn:         34405000000,
-				}},
-				TxOut: []*wire.TxOut{{
-					Value:    5000000,
-					Version:  0,
-					PkScript: hexToBytes("76a914f419b8db4ba65f3b6fcc233acb762ca6f51c23d488ac"),
-				}, {
-					Value:    34400000000,
-					Version:  0,
-					PkScript: hexToBytes("76a914cadf4fc336ab3c6a4610b75f31ba0676b7f663d288ac"),
-				}},
-				LockTime: 0,
-				Expiry:   0,
+			LockTime: 0,
+			Expiry:   0,
+		}, {
+			SerType: wire.TxSerializeFull,
+			Version: 1,
+			TxIn: []*wire.TxIn{{
+				PreviousOutPoint: wire.OutPoint{
+					Hash:  chainhash.Hash{},
+					Index: 0xffffffff,
+					Tree:  0,
+				},
+				SignatureScript: hexToBytes("0000"),
+				Sequence:        0xffffffff,
+				BlockHeight:     wire.NullBlockHeight,
+				BlockIndex:      wire.NullBlockIndex,
+				ValueIn:         159626785,
 			}, {
-				SerType: wire.TxSerializeFull,
-				Version: 1,
-				TxIn: []*wire.TxIn{{
-					PreviousOutPoint: wire.OutPoint{
-						Hash:  *mustParseHash("92fbe1d4be82f765dfabc9559d4620864b05cc897c4db0e29adac92d294e52b7"),
-						Index: 0,
-						Tree:  0,
-					},
-					SignatureScript: hexToBytes("483045022100e256743154c097465cf13e89955e1c9ff2e55c46051b627751dee0144183157e02201d8d4f02cde8496aae66768f94d35ce54465bd4ae8836004992d3216a93a13f00141049d23ce8686fe9b802a7a938e8952174d35dd2c2089d4112001ed8089023ab4f93a3c9fcd5bfeaa9727858bf640dc1b1c05ec3b434bb59837f8640e8810e87742"),
-					Sequence:        0xffffffff,
-					BlockHeight:     3214,
-					BlockIndex:      1234,
-					ValueIn:         13761000000,
-				}},
-				TxOut: []*wire.TxOut{{
-					Value:    5000000,
-					Version:  0,
-					PkScript: hexToBytes("76a914a983ad7c92c38fc0e2025212e9f972204c6e687088ac"),
-				}, {
-					Value:    13756000000,
-					Version:  0,
-					PkScript: hexToBytes("76a914a6ebd69952ab486a7a300bfffdcb395dc7d47c2388ac"),
-				}},
-				LockTime: 0,
-				Expiry:   0,
-			}},
-			utxoView: &UtxoViewpoint{entries: map[chainhash.Hash]*UtxoEntry{
-				*mustParseHash("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"): {
-					txVersion:  1,
-					isCoinBase: false,
-					hasExpiry:  false,
-					height:     321,
-					index:      123,
-					sparseOutputs: map[uint32]*utxoOutput{
-						1: {
-							amount:        34405000000,
-							scriptVersion: 0,
-							pkScript:      hexToBytes("76a9142084541c3931677527a7eafe56fd90207c344eb088ac"),
-							compressed:    false,
-						},
-					},
+				PreviousOutPoint: wire.OutPoint{
+					Hash: *mustParseHash("d3bce77da2747baa85fb7ca4f6f8e123f31cd15ac691b" +
+						"2f82543780158587d3a"),
+					Index: 0,
+					Tree:  1,
 				},
+				SignatureScript: hexToBytes("483045022100cc166d42c07e7a59e4b5a4ee13c0" +
+					"c9f96a4cd1a7f566356b8d599dec7126e52a0220126e28c59113efc66d28e04f8b" +
+					"4dc82538304f934ba4039831b5e814f69a2344012102a26ab1e011211185fe3583" +
+					"bc6393e713f65751cf000c00d9b80e12610cbb5a92ab9b359688ac"),
+				Sequence:    0xffffffff,
+				BlockHeight: 85314,
+				BlockIndex:  6,
+				ValueIn:     4294959555,
 			}},
-			serialized: hexToBytes("400001b2fb57eadf61e106a100a7445a8c3f67898841ec010000016edbc6c4d31bae9f1ccc38538a114bf42de65e86"),
-		},
-		{
-			name: "One tx, two inputs from same tx, neither spend last output",
-			entry: []spentTxOut{{
-				amount:        159747816,
-				height:        1111,
-				index:         2222,
-				scriptVersion: 0,
-				pkScript:      hexToBytes("4151"),
-				compressed:    true,
+			TxOut: []*wire.TxOut{{
+				Value:   0,
+				Version: 0,
+				PkScript: hexToBytes("6a24014c0832ff0ce236cd774274772f2e31aab07dda514" +
+					"36cbd6d03000000000000aa870100"),
 			}, {
-				amount:        159747816,
-				height:        3333,
-				index:         4444,
-				scriptVersion: 0,
-				pkScript:      hexToBytes("4151"),
-				compressed:    true,
-			}},
-			blockTxns: []*wire.MsgTx{{ // Coinbase omitted.
-				SerType: wire.TxSerializeFull,
-				Version: 1,
-				TxIn: []*wire.TxIn{{
-					PreviousOutPoint: wire.OutPoint{
-						Hash:  *mustParseHash("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"),
-						Index: 1,
-					},
-					SignatureScript: hexToBytes(""),
-					Sequence:        0xffffffff,
-					BlockHeight:     1111,
-					BlockIndex:      2222,
-					ValueIn:         159747816,
-				}, {
-					PreviousOutPoint: wire.OutPoint{
-						Hash:  *mustParseHash("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"),
-						Index: 2,
-					},
-					SignatureScript: hexToBytes(""),
-					Sequence:        0xffffffff,
-					BlockHeight:     3333,
-					BlockIndex:      4444,
-					ValueIn:         159747816,
-				}},
-				TxOut: []*wire.TxOut{{
-					Value:    165125632,
-					Version:  0,
-					PkScript: hexToBytes("51"),
-				}, {
-					Value:    154370000,
-					Version:  0,
-					PkScript: hexToBytes("51"),
-				}},
-				LockTime: 0,
-				Expiry:   0,
-			}},
-			utxoView: &UtxoViewpoint{entries: map[chainhash.Hash]*UtxoEntry{
-				*mustParseHash("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"): {
-					txVersion:  1,
-					isCoinBase: false,
-					hasExpiry:  false,
-					height:     100000,
-					sparseOutputs: map[uint32]*utxoOutput{
-						0: {
-							amount:   165712179,
-							pkScript: hexToBytes("51"),
-						},
-					},
-				},
+				Value:    0,
+				Version:  0,
+				PkScript: hexToBytes("6a06010002000000"),
+			}, {
+				Value:   4454586340,
+				Version: 0,
+				PkScript: hexToBytes("bb76a9149ac39159847e259c9162405b5f6c8135d2c7eaf" +
+					"188ac"),
 			}},
-			serialized: hexToBytes("0000415100004151"),
-		},
-	}
+			LockTime: 0,
+			Expiry:   0,
+		}},
+		serialized: hexToBytes("06005aba76a914a13afb81d54c9f8bb0c5e082d56fd563ab9" +
+			"b359688ac03808efefade57001aba76a914a13afb81d54c9f8bb0c5e082d56fd563ab9" +
+			"b359688ac0000206a1e9ac39159847e259c9162405b5f6c8135d2c7eaf1a3750400010" +
+			"00000005800001abd76a914000000000000000000000000000000000000000088ac010" +
+			"000b3c2069c496bc13228c154b03993809f278233d1"),
+	}}
 
 	for i, test := range tests {
 		// Ensure the journal entry serializes to the expected value.
@@ -891,297 +822,173 @@ func TestSpendJournalErrors(t *testing.T) {
 func TestUtxoSerialization(t *testing.T) {
 	t.Parallel()
 
+	// Define constants for indicating flags.
+	const (
+		noCoinbase   = false
+		withCoinbase = true
+		noExpiry     = false
+		withExpiry   = true
+		unspent      = false
+		spent        = true
+		unmodified   = false
+	)
+
 	tests := []struct {
 		name       string
 		entry      *UtxoEntry
 		serialized []byte
+		txOutIndex uint32
 	}{
 		{
-			name: "Only output 0, coinbase, even uncomp pubkey",
-			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: true,
-				hasExpiry:  false,
-				txType:     0,
-				height:     12345,
-				index:      54321,
-				stakeExtra: nil,
-				sparseOutputs: map[uint32]*utxoOutput{
-					0: {
-						amount:        5000000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"),
-						compressed:    false,
-					},
-				},
-			},
-			serialized: hexToBytes("01df3982a731010132000496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52"),
-		},
-		{
-			name: "Only output 0, coinbase, odd uncomp pubkey",
+			name: "Coinbase, even uncomp pubkey",
 			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: true,
-				hasExpiry:  false,
-				txType:     0,
-				height:     12345,
-				index:      54321,
-				stakeExtra: nil,
-				sparseOutputs: map[uint32]*utxoOutput{
-					0: {
-						amount:        5000000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52258a76c86aea2b1f59fb07ebe87e19dd6b8dee99409de18c57d340dbbd37a341ac"),
-						compressed:    false,
-					},
-				},
-			},
-			serialized: hexToBytes("01df3982a731010132000596b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52"),
-		},
-		{
-			name: "Only output 1, not coinbase",
-			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: false,
-				hasExpiry:  false,
-				txType:     0,
-				height:     55555,
-				index:      1,
-				stakeExtra: nil,
-				sparseOutputs: map[uint32]*utxoOutput{
-					1: {
-						amount:        1000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"),
-						compressed:    false,
-					},
-				},
-			},
-			serialized: hexToBytes("0182b103010002070000ee8bd501094a7d5ca318da2506de35e1cb025ddc"),
-		},
-		{
-			name: "Ticket with one output",
-			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: false,
-				hasExpiry:  false,
-				txType:     1,
-				height:     55555,
-				index:      1,
-				stakeExtra: hexToBytes("030f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588ac0000206a1e1a221182c26bbae681e4d96d452794e1951e70a208520000000000000054b5f466001abd76a9146c4f8b15918566534d134be7d7004b7f481bf36988ac"),
-				sparseOutputs: map[uint32]*utxoOutput{
-					1: {
-						amount:        1000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"),
-						compressed:    false,
-					},
-				},
+				amount: 5000000000,
+				pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a6" +
+					"27c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621" +
+					"e73a82cbf2342c858eeac"),
+				blockHeight:   12345,
+				blockIndex:    54321,
+				scriptVersion: 0,
+				packedFlags: encodeUtxoFlags(
+					withCoinbase,
+					unspent,
+					unmodified,
+					noExpiry,
+					stake.TxTypeRegular,
+				),
 			},
-			serialized: hexToBytes("0182b103010402070000ee8bd501094a7d5ca318da2506de35e1cb025ddc030f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588ac0000206a1e1a221182c26bbae681e4d96d452794e1951e70a208520000000000000054b5f466001abd76a9146c4f8b15918566534d134be7d7004b7f481bf36988ac"),
-		},
-		{
-			name: "Only output 2, coinbase, non-zero script version",
+			serialized: hexToBytes("df3982a7310132000496b538e853519c726a2c91e61ec11" +
+				"600ae1390813a627c66fb8be7947be63c52"),
+			txOutIndex: 0,
+		}, {
+			name: "Coinbase, odd uncomp pubkey",
 			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: true,
-				hasExpiry:  false,
-				txType:     0,
-				height:     12345,
-				index:      1,
-				sparseOutputs: map[uint32]*utxoOutput{
-					2: {
-						amount:        100937281,
-						scriptVersion: 0xffff,
-						pkScript:      hexToBytes("76a914da33f77cee27c2a975ed5124d7e4f7f97513510188ac"),
-						compressed:    false,
-					},
-				},
-			},
-			serialized: hexToBytes("01df390101000182b095bf4182fe7f00da33f77cee27c2a975ed5124d7e4f7f975135101"),
-		},
-		{
-			name: "outputs 0 and 2 not coinbase",
-			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: false,
-				hasExpiry:  false,
-				txType:     0,
-				height:     99999,
-				index:      3,
-				sparseOutputs: map[uint32]*utxoOutput{
-					0: {
-						amount:        20000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
-						compressed:    false,
-					},
-					2: {
-						amount:        15000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"),
-						compressed:    false,
-					},
-				},
+				amount: 5000000000,
+				pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a6" +
+					"27c66fb8be7947be63c52258a76c86aea2b1f59fb07ebe87e19dd6b8dee99409de" +
+					"18c57d340dbbd37a341ac"),
+				blockHeight:   12345,
+				blockIndex:    54321,
+				scriptVersion: 0,
+				packedFlags: encodeUtxoFlags(
+					withCoinbase,
+					unspent,
+					unmodified,
+					noExpiry,
+					stake.TxTypeRegular,
+				),
 			},
-			serialized: hexToBytes("01858c1f03000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a80090000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
-		},
-		{
-			name: "outputs 0 and 2 not coinbase, has expiry",
+			serialized: hexToBytes("df3982a7310132000596b538e853519c726a2c91e61ec11" +
+				"600ae1390813a627c66fb8be7947be63c52"),
+			txOutIndex: 0,
+		}, {
+			name: "Non-coinbase regular tx",
 			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: false,
-				hasExpiry:  true,
-				txType:     0,
-				height:     99999,
-				index:      3,
-				sparseOutputs: map[uint32]*utxoOutput{
-					0: {
-						amount:        20000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
-						compressed:    false,
-					},
-					2: {
-						amount:        15000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"),
-						compressed:    false,
-					},
-				},
+				amount: 1000000,
+				pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc8" +
+					"8ac"),
+				blockHeight:   55555,
+				blockIndex:    1,
+				scriptVersion: 0,
+				packedFlags: encodeUtxoFlags(
+					noCoinbase,
+					unspent,
+					unmodified,
+					noExpiry,
+					stake.TxTypeRegular,
+				),
 			},
-			serialized: hexToBytes("01858c1f03020501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a80090000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
-		},
-		{
-			name: "outputs 0 and 2, not coinbase, 1 marked spent",
+			serialized: hexToBytes("82b1030100070000ee8bd501094a7d5ca318da2506de35e" +
+				"1cb025ddc"),
+			txOutIndex: 0,
+		}, {
+			name: "Ticket tx",
 			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: false,
-				hasExpiry:  false,
-				txType:     0,
-				height:     12345,
-				index:      1,
-				sparseOutputs: map[uint32]*utxoOutput{
-					0: {
-						amount:        20000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
-						compressed:    false,
-					},
-					1: { // This won't be serialized.
-						spent:         true,
-						amount:        1000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914e43031c3e46f20bf1ccee9553ce815de5a48467588ac"),
-						compressed:    false,
-					},
-					2: {
-						amount:        15000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac"),
-						compressed:    false,
-					},
+				amount: 1000000,
+				pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc8" +
+					"8ac"),
+				blockHeight:   55555,
+				blockIndex:    1,
+				scriptVersion: 0,
+				packedFlags: encodeUtxoFlags(
+					noCoinbase,
+					unspent,
+					unmodified,
+					withExpiry,
+					stake.TxTypeSStx,
+				),
+				ticketMinOuts: &ticketMinimalOutputs{
+					data: hexToBytes("030f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0" +
+						"933850588ac0000206a1e1a221182c26bbae681e4d96d452794e1951e70a2085" +
+						"20000000000000054b5f466001abd76a9146c4f8b15918566534d134be7d7004" +
+						"b7f481bf36988ac"),
 				},
 			},
-			serialized: hexToBytes("01df3901000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a80090000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
-		},
-		{
-			name: "outputs 0 and 2, not coinbase, output 2 compressed",
+			serialized: hexToBytes("82b1030106070000ee8bd501094a7d5ca318da2506de35e" +
+				"1cb025ddc030f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588a" +
+				"c0000206a1e1a221182c26bbae681e4d96d452794e1951e70a208520000000000000" +
+				"054b5f466001abd76a9146c4f8b15918566534d134be7d7004b7f481bf36988ac"),
+			txOutIndex: 0,
+		}, {
+			name: "Output 2, coinbase, non-zero script version",
 			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: false,
-				hasExpiry:  false,
-				txType:     0,
-				height:     12345,
-				index:      1,
-				sparseOutputs: map[uint32]*utxoOutput{
-					0: {
-						amount:        20000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
-						compressed:    false,
-					},
-					2: {
-						// Uncompressed Amount: 15000000
-						// Uncompressed PkScript: 76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac
-						amount: 137,
-
-						pkScript:   hexToBytes("00b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
-						compressed: true,
-					},
-				},
+				amount: 100937281,
+				pkScript: hexToBytes("76a914da33f77cee27c2a975ed5124d7e4f7f9751351018" +
+					"8ac"),
+				blockHeight:   12345,
+				blockIndex:    1,
+				scriptVersion: 0xffff,
+				packedFlags: encodeUtxoFlags(
+					withCoinbase,
+					unspent,
+					unmodified,
+					noExpiry,
+					stake.TxTypeRegular,
+				),
 			},
-			serialized: hexToBytes("01df3901000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a884f0000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
-		},
-		{
-			name: "outputs 0 and 2, not coinbase, output 2 compressed, packed indexes reversed",
+			serialized: hexToBytes("df39010182b095bf4182fe7f00da33f77cee27c2a975ed5" +
+				"124d7e4f7f975135101"),
+			txOutIndex: 2,
+		}, {
+			name: "Has expiry",
 			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: false,
-				hasExpiry:  false,
-				txType:     0,
-				height:     33333,
-				index:      21,
-				sparseOutputs: map[uint32]*utxoOutput{
-					0: {
-						amount:        20000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a88ac"),
-						compressed:    false,
-					},
-					2: {
-						// Uncompressed Amount: 15000000
-						// Uncompressed PkScript: 76a914b8025be1b3efc63b0ad48e7f9f10e87544528d5888ac
-						amount:        137,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("00b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
-						compressed:    true,
-					},
-				},
+				amount: 20000000,
+				pkScript: hexToBytes("76a914e2ccd6ec7c6e2e581349c77e067385fa8236bf8a8" +
+					"8ac"),
+				blockHeight:   99999,
+				blockIndex:    3,
+				scriptVersion: 0,
+				packedFlags: encodeUtxoFlags(
+					noCoinbase,
+					unspent,
+					unmodified,
+					withExpiry,
+					stake.TxTypeRegular,
+				),
 			},
-			serialized: hexToBytes("0181833515000501120000e2ccd6ec7c6e2e581349c77e067385fa8236bf8a884f0000b8025be1b3efc63b0ad48e7f9f10e87544528d58"),
-		},
-		{
-			name: "Only output 0, coinbase, fully spent",
+			serialized: hexToBytes("858c1f0302120000e2ccd6ec7c6e2e581349c77e067385f" +
+				"a8236bf8a"),
+			txOutIndex: 0,
+		}, {
+			name: "Coinbase, spent",
 			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: false,
-				hasExpiry:  false,
-				txType:     0,
-				height:     33333,
-				index:      231,
-				sparseOutputs: map[uint32]*utxoOutput{
-					0: {
-						spent:         true,
-						amount:        5000000000,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"),
-						compressed:    false,
-					},
-				},
+				amount: 5000000000,
+				pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a6" +
+					"27c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621" +
+					"e73a82cbf2342c858eeac"),
+				blockHeight:   33333,
+				blockIndex:    3,
+				scriptVersion: 0,
+				packedFlags: encodeUtxoFlags(
+					withCoinbase,
+					spent,
+					unmodified,
+					withExpiry,
+					stake.TxTypeRegular,
+				),
 			},
 			serialized: nil,
-		},
-		{
-			name: "Only output 22, not coinbase",
-			entry: &UtxoEntry{
-				txVersion:  1,
-				isCoinBase: false,
-				hasExpiry:  false,
-				txType:     0,
-				height:     3221,
-				index:      211,
-				sparseOutputs: map[uint32]*utxoOutput{
-					22: {
-						spent:         false,
-						amount:        366875659,
-						scriptVersion: 0,
-						pkScript:      hexToBytes("a9141dd46a006572d820e448e12d2bbb38640bc718e687"),
-						compressed:    false,
-					},
-				},
-			},
-			serialized: hexToBytes("019815805300080000108ba5b9e76300011dd46a006572d820e448e12d2bbb38640bc718e6"),
+			txOutIndex: 0,
 		},
 	}
 
@@ -1200,97 +1007,22 @@ func TestUtxoSerialization(t *testing.T) {
 			continue
 		}
 
-		// Don't try to deserialize if the test entry was fully spent
-		// since it will have a nil serialization.
-		if test.entry.IsFullySpent() {
+		// Don't try to deserialize if the test entry was spent since it will have a
+		// nil serialization.
+		if test.entry.IsSpent() {
 			continue
 		}
 
-		// Deserialize to a utxo entry.
-		utxoEntry, err := deserializeUtxoEntry(test.serialized)
+		// Ensure that the serialized bytes are decoded back to the expected utxo.
+		gotUtxo, err := deserializeUtxoEntry(test.serialized, test.txOutIndex)
 		if err != nil {
-			t.Errorf("deserializeUtxoEntry #%d (%s) unexpected "+
-				"error: %v", i, test.name, err)
-			continue
-		}
-
-		// Ensure that the deserialized utxo entry has the same
-		// properties for the containing transaction and block height.
-		if utxoEntry.TxVersion() != test.entry.TxVersion() {
-			t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+
-				" txVersion: got %d, want %d", i, test.name,
-				utxoEntry.TxVersion(), test.entry.TxVersion())
-			continue
-		}
-		if utxoEntry.IsCoinBase() != test.entry.IsCoinBase() {
-			t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+
-				"coinbase flag: got %v, want %v", i, test.name,
-				utxoEntry.IsCoinBase(), test.entry.IsCoinBase())
-			continue
-		}
-		if utxoEntry.BlockHeight() != test.entry.BlockHeight() {
-			t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+
-				"block height: got %d, want %d", i, test.name,
-				utxoEntry.BlockHeight(),
-				test.entry.BlockHeight())
-			continue
-		}
-		if utxoEntry.IsFullySpent() != test.entry.IsFullySpent() {
-			t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+
-				"fully spent: got %v, want %v", i, test.name,
-				utxoEntry.IsFullySpent(),
-				test.entry.IsFullySpent())
-			continue
-		}
-
-		// Ensure all of the outputs in the test entry match the
-		// spentness of the output in the deserialized entry and the
-		// deserialized entry does not contain any additional utxos.
-		var numUnspent int
-		for outputIndex := range test.entry.sparseOutputs {
-			gotSpent := utxoEntry.IsOutputSpent(outputIndex)
-			wantSpent := test.entry.IsOutputSpent(outputIndex)
-			if !wantSpent {
-				numUnspent++
-			}
-			if gotSpent != wantSpent {
-				t.Errorf("deserializeUtxoEntry #%d (%s) output "+
-					"#%d: mismatched spent: got %v, want "+
-					"%v", i, test.name, outputIndex,
-					gotSpent, wantSpent)
-				continue
-			}
-		}
-		if len(utxoEntry.sparseOutputs) != numUnspent {
-			t.Errorf("deserializeUtxoEntry #%d (%s): mismatched "+
-				"number of unspent outputs: got %d, want %d", i,
-				test.name, len(utxoEntry.sparseOutputs),
-				numUnspent)
+			t.Errorf("serializeUtxoEntry #%d (%s): unexpected error: %v", i,
+				test.name, err)
 			continue
 		}
-
-		// Ensure all of the amounts and scripts of the utxos in the
-		// deserialized entry match the ones in the test entry.
-		for outputIndex := range utxoEntry.sparseOutputs {
-			gotAmount := utxoEntry.AmountByIndex(outputIndex)
-			wantAmount := test.entry.AmountByIndex(outputIndex)
-			if gotAmount != wantAmount {
-				t.Errorf("deserializeUtxoEntry #%d (%s) "+
-					"output #%d: mismatched amounts: got "+
-					"%d, want %d", i, test.name,
-					outputIndex, gotAmount, wantAmount)
-				continue
-			}
-
-			gotPkScript := utxoEntry.PkScriptByIndex(outputIndex)
-			wantPkScript := test.entry.PkScriptByIndex(outputIndex)
-			if !bytes.Equal(gotPkScript, wantPkScript) {
-				t.Errorf("deserializeUtxoEntry #%d (%s) "+
-					"output #%d mismatched scripts: got "+
-					"%x, want %x", i, test.name,
-					outputIndex, gotPkScript, wantPkScript)
-				continue
-			}
+		if !reflect.DeepEqual(gotUtxo, test.entry) {
+			t.Errorf("serializeUtxoEntry #%d (%s):\nwant: %+v\n got: %+v\n", i,
+				test.name, test.entry, gotUtxo)
 		}
 	}
 }
@@ -1303,39 +1035,96 @@ func TestUtxoEntryDeserializeErrors(t *testing.T) {
 	tests := []struct {
 		name       string
 		serialized []byte
+		txOutIndex uint32
 		errType    error
-	}{
-		{
-			name:       "no data after version",
-			serialized: hexToBytes("01"),
-			errType:    errDeserialize(""),
-		},
-		{
-			name:       "no data after block height",
-			serialized: hexToBytes("0101"),
-			errType:    errDeserialize(""),
-		},
-		{
-			name:       "no data after header code",
-			serialized: hexToBytes("010102"),
-			errType:    errDeserialize(""),
-		},
-		{
-			name:       "not enough bytes for unspentness bitmap",
-			serialized: hexToBytes("01017800"),
-			errType:    errDeserialize(""),
-		},
-		{
-			name:       "incomplete compressed txout",
-			serialized: hexToBytes("01010232"),
-			errType:    errDeserialize(""),
-		},
-	}
+	}{{
+		// [EOF]
+		name:       "nothing serialized (no block height)",
+		serialized: hexToBytes("01"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}, {
+		// [<block height 01> EOF]
+		name:       "no data after block height",
+		serialized: hexToBytes("01"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}, {
+		// [<block height 01> <block index 01> EOF]
+		name:       "no data after block index",
+		serialized: hexToBytes("0101"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}, {
+		// [<block height 01> <block index 01> <flags 01> EOF]
+		name:       "no data after flags",
+		serialized: hexToBytes("010101"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}, {
+		// [<block height 01> <block index 01> <flags 01> <compressed amount 49>
+		//  <script version 00> <compressed pk script 12> EOF]
+		name:       "incomplete compressed txout",
+		serialized: hexToBytes("010101490012"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}, {
+		// [<block height 01> <block index 01> <flags 06> <compressed amount 49>
+		//  <script version 00> <compressed pk script 01 6e ...> EOF]
+		name: "no minimal output data after script for a ticket submission " +
+			"output",
+		serialized: hexToBytes("0101064900016edbc6c4d31bae9f1ccc38538a114bf42de65" +
+			"e86"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}, {
+		// [<block height 01> <block index 01> <flags 06> <compressed amount 49>
+		//  <script version 00> <compressed pk script 01 6e ...>
+		//  <ticket min outs {num outputs 01}> EOF]
+		name: "truncated minimal output data after script for a ticket " +
+			"submission output (num outputs only)",
+		serialized: hexToBytes("0101064900016edbc6c4d31bae9f1ccc38538a114bf42de65" +
+			"e8601"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}, {
+		// [<block height 01> <block index 01> <flags 06> <compressed amount 49>
+		//  <script version 00> <compressed pk script 01 6e ...>
+		//  <ticket min outs {num outputs 01} {amount 0f}> EOF]
+		name: "truncated minimal output data after script for a ticket " +
+			"submission output (num outputs and amount only)",
+		serialized: hexToBytes("0101064900016edbc6c4d31bae9f1ccc38538a114bf42de65" +
+			"e86010f"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}, {
+		// [<block height 01> <block index 01> <flags 06> <compressed amount 49>
+		//  <script version 00> <compressed pk script 01 6e ...>
+		//  <ticket min outs {num outputs 01} {amount 0f} {script version 00}> EOF]
+		name: "truncated minimal output data after script for a ticket " +
+			"submission output (num outputs, amount, and script version only)",
+		serialized: hexToBytes("0101064900016edbc6c4d31bae9f1ccc38538a114bf42de65" +
+			"e86010f00"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}, {
+		// [<block height 01> <block index 01> <flags 06> <compressed amount 49>
+		//  <script version 00> <compressed pk script 01 6e ...>
+		//  <ticket min outs {num outputs 01} {amount 0f} {script version 00}
+		//  {script size 1a} {25 bytes of script instead of 26}> EOF]
+		name: "truncated minimal output data after script for a ticket " +
+			"submission output (script size specified as 0x1a, but only 0x19 bytes " +
+			"provided)",
+		serialized: hexToBytes("0101064900016edbc6c4d31bae9f1ccc38538a114bf42de65" +
+			"e86010f001aba76a9140cdf9941c0c221243cb8672cd1ad2c4c0933850588"),
+		txOutIndex: 0,
+		errType:    errDeserialize(""),
+	}}
 
 	for _, test := range tests {
 		// Ensure the expected error type is returned and the returned
 		// entry is nil.
-		entry, err := deserializeUtxoEntry(test.serialized)
+		entry, err := deserializeUtxoEntry(test.serialized, test.txOutIndex)
 		if !errors.As(err, &test.errType) {
 			t.Errorf("deserializeUtxoEntry (%s): expected error "+
 				"type does not match - got %T, want %T",
diff --git a/blockchain/compress_test.go b/blockchain/compress_test.go
index 84af9cce..81885b9e 100644
--- a/blockchain/compress_test.go
+++ b/blockchain/compress_test.go
@@ -368,100 +368,66 @@ func TestCompressedTxOut(t *testing.T) {
 		amount        uint64
 		scriptVersion uint16
 		pkScript      []byte
-		compPkScript  []byte
 		version       uint32
 		compressed    []byte
 		hasAmount     bool
-		isCompressed  bool
 	}{
 		{
 			name:          "nulldata with 0 DCR",
 			amount:        0,
 			scriptVersion: 0,
 			pkScript:      hexToBytes("6a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"),
-			compPkScript:  hexToBytes("626a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"),
 			version:       1,
 			compressed:    hexToBytes("00626a200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"),
 			hasAmount:     false,
-			isCompressed:  false,
 		},
 		{
 			name:          "pay-to-pubkey-hash dust, no amount",
 			amount:        0,
 			scriptVersion: 0,
 			pkScript:      hexToBytes("76a9141018853670f9f3b0582c5b9ee8ce93764ac32b9388ac"),
-			compPkScript:  hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
 			version:       1,
 			compressed:    hexToBytes("00001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
 			hasAmount:     false,
-			isCompressed:  false,
-		},
-		{
-			name:          "pay-to-pubkey-hash dust, no amount, precompressed",
-			amount:        0,
-			scriptVersion: 0,
-			pkScript:      hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
-			compPkScript:  hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
-			version:       1,
-			compressed:    hexToBytes("00001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
-			hasAmount:     false,
-			isCompressed:  true,
 		},
 		{
 			name:          "pay-to-pubkey-hash dust, amount",
 			amount:        546,
 			scriptVersion: 0,
 			pkScript:      hexToBytes("76a9141018853670f9f3b0582c5b9ee8ce93764ac32b9388ac"),
-			compPkScript:  hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
-			version:       1,
-			compressed:    hexToBytes("a52f00001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
-			hasAmount:     true,
-			isCompressed:  false,
-		},
-		{
-			name:          "pay-to-pubkey-hash dust, amount, precompressed",
-			amount:        546,
-			scriptVersion: 0,
-			pkScript:      hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
-			compPkScript:  hexToBytes("001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
 			version:       1,
 			compressed:    hexToBytes("a52f00001018853670f9f3b0582c5b9ee8ce93764ac32b93"),
 			hasAmount:     true,
-			isCompressed:  true,
 		},
 		{
 			name:          "pay-to-pubkey uncompressed, no amount",
 			amount:        0,
 			scriptVersion: 0,
 			pkScript:      hexToBytes("4104192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453eac"),
-			compPkScript:  hexToBytes("04192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"),
 			version:       1,
 			compressed:    hexToBytes("0004192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"),
 			hasAmount:     false,
-			isCompressed:  false,
 		},
 		{
 			name:          "pay-to-pubkey uncompressed 1 DCR, amount present",
 			amount:        100000000,
 			scriptVersion: 0,
 			pkScript:      hexToBytes("4104192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453eac"),
-			compPkScript:  hexToBytes("04192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"),
 			version:       1,
 			compressed:    hexToBytes("090004192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4"),
 			hasAmount:     true,
-			isCompressed:  false,
 		},
 	}
 
 	for _, test := range tests {
-		targetSz := compressedTxOutSize(0, test.scriptVersion, test.pkScript, currentCompressionVersion, test.isCompressed, test.hasAmount) - 1
+		targetSz := compressedTxOutSize(0, test.scriptVersion, test.pkScript, currentCompressionVersion, test.hasAmount) - 1
 		target := make([]byte, targetSz)
 		putCompressedScript(target, test.scriptVersion, test.pkScript, currentCompressionVersion)
 
 		// Ensure the function to calculate the serialized size without
 		// actually serializing the txout is calculated properly.
 		gotSize := compressedTxOutSize(test.amount, test.scriptVersion,
-			test.pkScript, test.version, test.isCompressed, test.hasAmount)
+			test.pkScript, test.version, test.hasAmount)
 		if gotSize != len(test.compressed) {
 			t.Errorf("compressedTxOutSize (%s): did not get "+
 				"expected size - got %d, want %d", test.name,
@@ -473,7 +439,7 @@ func TestCompressedTxOut(t *testing.T) {
 		gotCompressed := make([]byte, gotSize)
 		gotBytesWritten := putCompressedTxOut(gotCompressed,
 			test.amount, test.scriptVersion, test.pkScript,
-			test.version, test.isCompressed, test.hasAmount)
+			test.version, test.hasAmount)
 		if !bytes.Equal(gotCompressed, test.compressed) {
 			t.Errorf("compressTxOut (%s): did not get expected "+
 				"bytes - got %x, want %x", test.name,
@@ -510,10 +476,10 @@ func TestCompressedTxOut(t *testing.T) {
 				test.name, gotScrVersion, test.scriptVersion)
 			continue
 		}
-		if !bytes.Equal(gotScript, test.compPkScript) {
+		if !bytes.Equal(gotScript, test.pkScript) {
 			t.Errorf("decodeCompressedTxOut (%s): did not get "+
 				"expected script - got %x, want %x",
-				test.name, gotScript, test.compPkScript)
+				test.name, gotScript, test.pkScript)
 			continue
 		}
 		if gotBytesRead != len(test.compressed) {
@@ -522,21 +488,6 @@ func TestCompressedTxOut(t *testing.T) {
 				test.name, gotBytesRead, len(test.compressed))
 			continue
 		}
-
-		// Ensure the compressed values decompress to the expected
-		// txout.
-		gotScript = decompressScript(gotScript, test.version)
-		localScript := make([]byte, len(test.pkScript))
-		copy(localScript, test.pkScript)
-		if test.isCompressed {
-			localScript = decompressScript(localScript, test.version)
-		}
-		if !bytes.Equal(gotScript, localScript) {
-			t.Errorf("decompressTxOut (%s): did not get expected "+
-				"script - got %x, want %x", test.name,
-				gotScript, test.pkScript)
-			continue
-		}
 	}
 }
 
diff --git a/blockchain/error_test.go b/blockchain/error_test.go
index 96c0cab7..a8add038 100644
--- a/blockchain/error_test.go
+++ b/blockchain/error_test.go
@@ -102,7 +102,6 @@ func TestErrorKindStringer(t *testing.T) {
 		{ErrForceReorgWrongChain, "ErrForceReorgWrongChain"},
 		{ErrForceReorgMissingChild, "ErrForceReorgMissingChild"},
 		{ErrBadStakebaseValue, "ErrBadStakebaseValue"},
-		{ErrDiscordantTxTree, "ErrDiscordantTxTree"},
 		{ErrStakeFees, "ErrStakeFees"},
 		{ErrNoStakeTx, "ErrNoStakeTx"},
 		{ErrBadBlockHeight, "ErrBadBlockHeight"},
diff --git a/blockchain/fullblocktests/generate.go b/blockchain/fullblocktests/generate.go
index 6e4ee48d..3e6a01fe 100644
--- a/blockchain/fullblocktests/generate.go
+++ b/blockchain/fullblocktests/generate.go
@@ -2014,7 +2014,7 @@ func Generate(includeLargeReorg bool) (tests [][]TestInstance, err error) {
 		tx.TxIn[0].PreviousOutPoint.Tree = wire.TxTreeStake
 		b.AddTransaction(tx)
 	})
-	rejected(blockchain.ErrDiscordantTxTree)
+	rejected(blockchain.ErrMissingTxOut)
 
 	// Create block with no dev subsidy for coinbase transaction.
 	//
diff --git a/blockchain/utxoviewpoint_test.go b/blockchain/utxoviewpoint_test.go
index 95cda652..2d93c678 100644
--- a/blockchain/utxoviewpoint_test.go
+++ b/blockchain/utxoviewpoint_test.go
@@ -55,8 +55,8 @@ func TestFetchUtxoView(t *testing.T) {
 
 		for txInIdx, txIn := range tx.MsgTx().TxIn {
 			prevOut := &txIn.PreviousOutPoint
-			entry := view.LookupEntry(&prevOut.Hash)
-			gotSpent := entry == nil || entry.IsOutputSpent(prevOut.Index)
+			entry := view.LookupEntry(*prevOut)
+			gotSpent := entry == nil || entry.IsSpent()
 			if gotSpent != spent {
 				t.Fatalf("unexpected spent state for txo %s referenced by "+
 					"input %d -- got %v, want %v", prevOut, txInIdx,
diff --git a/internal/mempool/mempool_test.go b/internal/mempool/mempool_test.go
index 65ea82f9..9546d1cf 100644
--- a/internal/mempool/mempool_test.go
+++ b/internal/mempool/mempool_test.go
@@ -70,10 +70,12 @@ func (s *fakeChain) SetNextStakeDifficulty(nextStakeDiff int64) {
 	s.Unlock()
 }
 
-// FetchUtxoView loads utxo details about the input transactions referenced by
-// the passed transaction from the point of view of the fake chain.
-// It also attempts to fetch the utxo details for the transaction itself so the
-// returned view can be examined for duplicate unspent transaction outputs.
+// FetchUtxoView loads unspent transaction outputs for the inputs referenced by
+// the passed transaction from the point of view of the main chain tip while
+// taking into account whether or not the transactions in the regular tree of
+// the current tip block should be included or not depending on the provided
+// flag.  It also attempts to fetch the utxos for the outputs of the transaction
+// so the returned view can be examined for duplicate transactions.
 //
 // This function is safe for concurrent access however the returned view is NOT.
 func (s *fakeChain) FetchUtxoView(tx *dcrutil.Tx, treeValid bool) (*blockchain.UtxoViewpoint, error) {
@@ -83,16 +85,20 @@ func (s *fakeChain) FetchUtxoView(tx *dcrutil.Tx, treeValid bool) (*blockchain.U
 	// All entries are cloned to ensure modifications to the returned view
 	// do not affect the fake chain's view.
 
-	// Add an entry for the tx itself to the new view.
+	// Add entries for the outputs of the tx to the new view.
+	msgTx := tx.MsgTx()
 	viewpoint := blockchain.NewUtxoViewpoint(nil)
-	entry := s.utxos.LookupEntry(tx.Hash())
-	viewpoint.Entries()[*tx.Hash()] = entry.Clone()
+	outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tx.Tree()}
+	for txOutIdx := range msgTx.TxOut {
+		outpoint.Index = uint32(txOutIdx)
+		entry := s.utxos.LookupEntry(outpoint)
+		viewpoint.Entries()[outpoint] = entry.Clone()
+	}
 
 	// Add entries for all of the inputs to the tx to the new view.
-	for _, txIn := range tx.MsgTx().TxIn {
-		originHash := &txIn.PreviousOutPoint.Hash
-		entry := s.utxos.LookupEntry(originHash)
-		viewpoint.Entries()[*originHash] = entry.Clone()
+	for _, txIn := range msgTx.TxIn {
+		entry := s.utxos.LookupEntry(txIn.PreviousOutPoint)
+		viewpoint.Entries()[txIn.PreviousOutPoint] = entry.Clone()
 	}
 
 	return viewpoint, nil
@@ -194,7 +200,7 @@ func (s *fakeChain) CalcSequenceLock(tx *dcrutil.Tx, view *blockchain.UtxoViewpo
 			continue
 		}
 
-		utxo := view.LookupEntry(&txIn.PreviousOutPoint.Hash)
+		utxo := view.LookupEntry(txIn.PreviousOutPoint)
 		if utxo == nil {
 			str := fmt.Sprintf("output %v referenced from transaction %s:%d "+
 				"either does not exist or has already been spent",
@@ -1002,7 +1008,8 @@ func TestTicketPurchaseOrphan(t *testing.T) {
 	// Add the transaction back to the mempool to ensure it
 	// kicks the ticket out to the stage pool.
 	harness.AddFakeUTXO(tx, int64(mining.UnminedHeight))
-	harness.chain.utxos.LookupEntry(tx.Hash()).SpendOutput(0)
+	outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: wire.TxTreeRegular, Index: 0}
+	harness.chain.utxos.LookupEntry(outpoint).Spend()
 	_, err = harness.txPool.MaybeAcceptTransaction(tx, false, false)
 	if err != nil {
 		t.Fatalf("ProcessTransaction: failed to accept valid transaction %v",
diff --git a/internal/mining/mining_harness_test.go b/internal/mining/mining_harness_test.go
index 10156e94..ba70f603 100644
--- a/internal/mining/mining_harness_test.go
+++ b/internal/mining/mining_harness_test.go
@@ -132,24 +132,30 @@ func (c *fakeChain) CheckTSpendHasVotes(prevHash chainhash.Hash, tspend *dcrutil
 	return c.checkTSpendHasVotesErr
 }
 
-// FetchUtxoView loads utxo details about the input transactions referenced by
-// the passed transaction from the point of view of the fake chain.  It also
-// attempts to fetch the utxo details for the transaction itself so the
-// returned view can be examined for duplicate unspent transaction outputs.
+// FetchUtxoView loads unspent transaction outputs for the inputs referenced by
+// the passed transaction from the point of view of the main chain tip while
+// taking into account whether or not the transactions in the regular tree of
+// the current tip block should be included or not depending on the provided
+// flag.  It also attempts to fetch the utxos for the outputs of the transaction
+// so the returned view can be examined for duplicate transactions.
 func (c *fakeChain) FetchUtxoView(tx *dcrutil.Tx, treeValid bool) (*blockchain.UtxoViewpoint, error) {
 	// All entries are cloned to ensure modifications to the returned view
 	// do not affect the fake chain's view.
 
-	// Add an entry for the tx itself to the new view.
+	// Add entries for the outputs of the tx to the new view.
+	msgTx := tx.MsgTx()
 	viewpoint := blockchain.NewUtxoViewpoint(nil)
-	entry := c.utxos.LookupEntry(tx.Hash())
-	viewpoint.Entries()[*tx.Hash()] = entry.Clone()
+	prevOut := wire.OutPoint{Hash: *tx.Hash(), Tree: tx.Tree()}
+	for txOutIdx := range msgTx.TxOut {
+		prevOut.Index = uint32(txOutIdx)
+		entry := c.utxos.LookupEntry(prevOut)
+		viewpoint.Entries()[prevOut] = entry.Clone()
+	}
 
 	// Add entries for all of the inputs to the tx to the new view.
 	for _, txIn := range tx.MsgTx().TxIn {
-		originHash := &txIn.PreviousOutPoint.Hash
-		entry := c.utxos.LookupEntry(originHash)
-		viewpoint.Entries()[*originHash] = entry.Clone()
+		entry := c.utxos.LookupEntry(txIn.PreviousOutPoint)
+		viewpoint.Entries()[txIn.PreviousOutPoint] = entry.Clone()
 	}
 
 	return viewpoint, c.fetchUtxoViewErr
@@ -358,10 +364,10 @@ func (p *fakeTxSource) fetchRedeemers(outpoints map[wire.OutPoint]*dcrutil.Tx, t
 	tree := wire.TxTreeRegular
 	seen := map[chainhash.Hash]struct{}{}
 	redeemers := make([]*dcrutil.Tx, 0)
-	prevOut := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
+	outpoint := wire.OutPoint{Hash: *tx.Hash(), Tree: tree}
 	for i := uint32(0); i < uint32(len(tx.MsgTx().TxOut)); i++ {
-		prevOut.Index = i
-		txRedeemer, exists := outpoints[prevOut]
+		outpoint.Index = i
+		txRedeemer, exists := outpoints[outpoint]
 		if !exists {
 			continue
 		}
@@ -413,13 +419,7 @@ func (p *fakeTxSource) removeOrphan(tx *dcrutil.Tx, removeRedeemers bool, isTrea
 
 	// Remove any orphans that redeem outputs from this one if requested.
 	if removeRedeemers {
-		txType := stake.DetermineTxType(tx.MsgTx(), isTreasuryEnabled)
-		tree := wire.TxTreeRegular
-		if txType != stake.TxTypeRegular {
-			tree = wire.TxTreeStake
-		}
-
-		prevOut := wire.OutPoint{Hash: *txHash, Tree: tree}
+		prevOut := wire.OutPoint{Hash: *txHash, Tree: tx.Tree()}
 		for txOutIdx := range tx.MsgTx().TxOut {
 			prevOut.Index = uint32(txOutIdx)
 			for _, orphan := range p.orphansByPrev[prevOut] {
@@ -661,8 +661,8 @@ func (p *fakeTxSource) maybeAcceptTransaction(tx *dcrutil.Tx, isNew bool) ([]*ch
 			continue
 		}
 
-		entry := utxoView.LookupEntry(&txIn.PreviousOutPoint.Hash)
-		if entry == nil || entry.IsFullySpent() {
+		entry := utxoView.LookupEntry(txIn.PreviousOutPoint)
+		if entry == nil || entry.IsSpent() {
 			// Must make a copy of the hash here since the iterator
 			// is replaced and taking its address directly would
 			// result in all of the entries pointing to the same
@@ -678,10 +678,9 @@ func (p *fakeTxSource) maybeAcceptTransaction(tx *dcrutil.Tx, isNew bool) ([]*ch
 					txIn.PreviousOutPoint.Hash)
 				continue
 			}
-			if entry.IsFullySpent() {
-				log.Tracef("Transaction %v uses full spent input %v "+
-					"and will be considered an orphan", txHash,
-					txIn.PreviousOutPoint.Hash)
+			if entry.IsSpent() {
+				log.Tracef("Transaction %v uses spent input %v and will be considered "+
+					"an orphan", txHash, txIn.PreviousOutPoint.Hash)
 			}
 		}
 	}
@@ -878,19 +877,25 @@ func (p *fakeTxSource) fetchInputUtxos(tx *dcrutil.Tx, isTreasuryEnabled bool) (
 	}
 
 	// Attempt to populate any missing inputs from the transaction pool.
-	for originHash, entry := range utxoView.Entries() {
-		if entry != nil && !entry.IsFullySpent() {
+	for _, txIn := range tx.MsgTx().TxIn {
+		prevOut := &txIn.PreviousOutPoint
+		entry := utxoView.LookupEntry(*prevOut)
+		if entry != nil && !entry.IsSpent() {
 			continue
 		}
 
-		if poolTxDesc, exists := p.pool[originHash]; exists {
-			utxoView.AddTxOuts(poolTxDesc.Tx, UnminedHeight, wire.NullBlockIndex,
-				isTreasuryEnabled)
+		if poolTxDesc, exists := p.pool[prevOut.Hash]; exists {
+			// AddTxOut ignores out of range index values, so it is safe to call without
+			// bounds checking here.
+			utxoView.AddTxOut(poolTxDesc.Tx, prevOut.Index, UnminedHeight,
+				wire.NullBlockIndex, isTreasuryEnabled)
 		}
 
-		if stagedTx, exists := p.staged[originHash]; exists {
-			utxoView.AddTxOuts(stagedTx, UnminedHeight, wire.NullBlockIndex,
-				isTreasuryEnabled)
+		if stagedTx, exists := p.staged[prevOut.Hash]; exists {
+			// AddTxOut ignores out of range index values, so it is safe to call without
+			// bounds checking here.
+			utxoView.AddTxOut(stagedTx, prevOut.Index, UnminedHeight,
+				wire.NullBlockIndex, isTreasuryEnabled)
 		}
 	}
 
diff --git a/internal/mining/mining_view_test.go b/internal/mining/mining_view_test.go
index 6c3fb884..457a5666 100644
--- a/internal/mining/mining_view_test.go
+++ b/internal/mining/mining_view_test.go
@@ -497,7 +497,8 @@ func TestAncestorTrackingLimits(t *testing.T) {
 	for index := len(allTxns) - 1; index >= 0; index-- {
 		tx := allTxns[index]
 		txHash := tx.Hash()
-		harness.chain.utxos.LookupEntry(txHash).SpendOutput(0)
+		outpoint := wire.OutPoint{Hash: *txHash, Index: 0, Tree: wire.TxTreeRegular}
+		harness.chain.utxos.LookupEntry(outpoint).Spend()
 		_, err = harness.AddTransactionToTxSource(tx)
 		if err != nil {
 			t.Fatalf("unable to add transaction to the tx source: %v", err)
diff --git a/internal/rpcserver/rpcserverhandlers_test.go b/internal/rpcserver/rpcserverhandlers_test.go
index 0473da09..5cbe276a 100644
--- a/internal/rpcserver/rpcserverhandlers_test.go
+++ b/internal/rpcserver/rpcserverhandlers_test.go
@@ -56,17 +56,16 @@ const testDataPath = "testdata"
 
 // testRPCUtxoEntry provides a mock utxo entry by implementing the UtxoEntry interface.
 type testRPCUtxoEntry struct {
-	amountByIndex        int64
+	amount               int64
 	hasExpiry            bool
 	height               uint32
 	index                uint32
 	isCoinBase           bool
-	isOutputSpent        bool
-	modified             bool
-	pkScriptByIndex      []byte
-	scriptVersionByIndex uint16
+	isSpent              bool
+	pkScript             []byte
+	scriptVersion        uint16
+	ticketMinimalOutputs []*stake.MinimalOutput
 	txType               stake.TxType
-	txVersion            uint16
 }
 
 // ToUtxoEntry returns a mocked underlying UtxoEntry instance.
@@ -74,49 +73,50 @@ func (u *testRPCUtxoEntry) ToUtxoEntry() *blockchain.UtxoEntry {
 	return nil
 }
 
-// TransactionType returns a mocked txType of the testRPCUtxoEntry.
+// TransactionType returns a mocked type of the transaction that the output is
+// contained in.
 func (u *testRPCUtxoEntry) TransactionType() stake.TxType {
 	return u.txType
 }
 
-// IsOutputSpent returns a mocked bool representing whether or not the provided
-// output index has been spent.
-func (u *testRPCUtxoEntry) IsOutputSpent(outputIndex uint32) bool {
-	return u.isOutputSpent
+// IsSpent returns a mocked bool representing whether or not the output has been
+// spent.
+func (u *testRPCUtxoEntry) IsSpent() bool {
+	return u.isSpent
 }
 
-// BlockHeight returns a mocked height of the testRPCUtxoEntry.
+// BlockHeight returns a mocked height of the block containing the output.
 func (u *testRPCUtxoEntry) BlockHeight() int64 {
 	return int64(u.height)
 }
 
-// TxVersion returns a mocked txVersion of the testRPCUtxoEntry.
-func (u *testRPCUtxoEntry) TxVersion() uint16 {
-	return u.txVersion
+// Amount returns a mocked amount of the output.
+func (u *testRPCUtxoEntry) Amount() int64 {
+	return u.amount
 }
 
-// AmountByIndex returns a mocked amount of the provided output index.
-func (u *testRPCUtxoEntry) AmountByIndex(outputIndex uint32) int64 {
-	return u.amountByIndex
+// ScriptVersion returns a mocked public key script version of the output.
+func (u *testRPCUtxoEntry) ScriptVersion() uint16 {
+	return u.scriptVersion
 }
 
-// ScriptVersionByIndex returns a mocked public key script for the provided
-// output index.
-func (u *testRPCUtxoEntry) ScriptVersionByIndex(outputIndex uint32) uint16 {
-	return u.scriptVersionByIndex
+// PkScript returns a mocked public key script of the output.
+func (u *testRPCUtxoEntry) PkScript() []byte {
+	return u.pkScript
 }
 
-// PkScriptByIndex returns a mocked public key script for the provided output
-// index.
-func (u *testRPCUtxoEntry) PkScriptByIndex(outputIndex uint32) []byte {
-	return u.pkScriptByIndex
-}
-
-// IsCoinBase returns a mocked isCoinBase bool of the testRPCUtxoEntry.
+// IsCoinBase returns a mocked bool representing whether or not the output was
+// contained in a coinbase transaction.
 func (u *testRPCUtxoEntry) IsCoinBase() bool {
 	return u.isCoinBase
 }
 
+// TicketMinimalOutputs returns mocked minimal outputs for the ticket
+// transaction that the output is contained in.
+func (u *testRPCUtxoEntry) TicketMinimalOutputs() []*stake.MinimalOutput {
+	return u.ticketMinimalOutputs
+}
+
 // tspendVotes is used to mock the results of a chain TSpendCountVotes call.
 type tspendVotes struct {
 	yes int64
@@ -145,7 +145,6 @@ type testRPCChain struct {
 	checkLiveTicket               bool
 	checkLiveTickets              []bool
 	checkMissedTickets            []bool
-	convertUtxosToMinimalOutputs  []*stake.MinimalOutput
 	countVoteVersion              uint32
 	countVoteVersionErr           error
 	estimateNextStakeDifficultyFn func(hash *chainhash.Hash, newTickets int64, useMaxTickets bool) (diff int64, err error)
@@ -263,11 +262,6 @@ func (c *testRPCChain) CheckMissedTickets(hashes []chainhash.Hash) []bool {
 	return c.checkMissedTickets
 }
 
-// ConvertUtxosToMinimalOutputs returns a mocked MinimalOutput slice.
-func (c *testRPCChain) ConvertUtxosToMinimalOutputs(entry UtxoEntry) []*stake.MinimalOutput {
-	return c.convertUtxosToMinimalOutputs
-}
-
 // CountVoteVersion returns a mocked total number of version votes for the current
 // rule change activation interval.
 func (c *testRPCChain) CountVoteVersion(version uint32) (uint32, error) {
@@ -280,7 +274,7 @@ func (c *testRPCChain) EstimateNextStakeDifficulty(hash *chainhash.Hash, newTick
 }
 
 // FetchUtxoEntry returns a mocked UtxoEntry.
-func (c *testRPCChain) FetchUtxoEntry(txHash *chainhash.Hash) (UtxoEntry, error) {
+func (c *testRPCChain) FetchUtxoEntry(outpoint wire.OutPoint) (UtxoEntry, error) {
 	return c.fetchUtxoEntry, c.fetchUtxoEntryErr
 }
 
@@ -1393,19 +1387,6 @@ func defaultMockRPCChain() *testRPCChain {
 			Status:    "active",
 		}},
 		chainWork: chainWork,
-		convertUtxosToMinimalOutputs: []*stake.MinimalOutput{{
-			PkScript: hexToBytes("baa914780239ea1231ba67b0c5b82e786b51e21072522187"),
-			Value:    100000000,
-			Version:  0,
-		}, {
-			PkScript: hexToBytes("6a1e355c96f48612d57509140e9a049981d5f9970f945c770d00000000000058"),
-			Value:    0,
-			Version:  0,
-		}, {
-			PkScript: hexToBytes("bd76a914000000000000000000000000000000000000000088ac"),
-			Value:    0,
-			Version:  0,
-		}},
 		estimateNextStakeDifficultyFn: func(*chainhash.Hash, int64, bool) (int64, error) {
 			return 14336790201, nil
 		},
@@ -1413,7 +1394,19 @@ func defaultMockRPCChain() *testRPCChain {
 			hasExpiry: true,
 			height:    100000,
 			txType:    stake.TxTypeSStx,
-			txVersion: 1,
+			ticketMinimalOutputs: []*stake.MinimalOutput{{
+				PkScript: hexToBytes("baa914780239ea1231ba67b0c5b82e786b51e21072522187"),
+				Value:    100000000,
+				Version:  0,
+			}, {
+				PkScript: hexToBytes("6a1e355c96f48612d57509140e9a049981d5f9970f945c770d00000000000058"),
+				Value:    0,
+				Version:  0,
+			}, {
+				PkScript: hexToBytes("bd76a914000000000000000000000000000000000000000088ac"),
+				Value:    0,
+				Version:  0,
+			}},
 		},
 		fetchUtxoStats: &blockchain.UtxoStats{
 			Utxos:          1593879,
@@ -2129,34 +2122,39 @@ func TestHandleCreateRawSSRtx(t *testing.T) {
 		},
 		mockChain: func() *testRPCChain {
 			chain := defaultMockRPCChain()
-			chain.convertUtxosToMinimalOutputs = []*stake.MinimalOutput{{
-				PkScript: []byte{
-					0xBA, 0xA9, 0x14, 0x78, 0x02, 0x39, 0xEA, 0x12,
-					0x31, 0xBA, 0x67, 0xB0, 0xC5, 0xB8, 0x2E, 0x78,
-					0x6B, 0x51, 0xE2, 0x10, 0x72, 0x52, 0x21, 0x87,
-				},
-				Value:   100000000,
-				Version: 0,
-			}, {
-				PkScript: []byte{
-					0x6A, 0x1E, 0x35, 0x5C, 0x96, 0xF4, 0x86, 0x12,
-					0xD5, 0x75, 0x09, 0x14, 0x0E, 0x9A, 0x04, 0x99,
-					0x81, 0xD5, 0xF9, 0x97, 0x0F, 0x94,
-					0x5C, 0x77, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x80, // commitamt (set MSB for P2SH)
-					0x00, 0x58,
-				},
-				Value:   0,
-				Version: 0,
-			}, {
-				PkScript: []byte{
-					0xBD, 0x76, 0xA9, 0x14, 0x00, 0x00, 0x00, 0x00,
-					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-					0x88, 0xAC,
-				},
-				Value:   0,
-				Version: 0,
-			}}
+			chain.fetchUtxoEntry = &testRPCUtxoEntry{
+				hasExpiry: true,
+				height:    100000,
+				txType:    stake.TxTypeSStx,
+				ticketMinimalOutputs: []*stake.MinimalOutput{{
+					PkScript: []byte{
+						0xBA, 0xA9, 0x14, 0x78, 0x02, 0x39, 0xEA, 0x12,
+						0x31, 0xBA, 0x67, 0xB0, 0xC5, 0xB8, 0x2E, 0x78,
+						0x6B, 0x51, 0xE2, 0x10, 0x72, 0x52, 0x21, 0x87,
+					},
+					Value:   100000000,
+					Version: 0,
+				}, {
+					PkScript: []byte{
+						0x6A, 0x1E, 0x35, 0x5C, 0x96, 0xF4, 0x86, 0x12,
+						0xD5, 0x75, 0x09, 0x14, 0x0E, 0x9A, 0x04, 0x99,
+						0x81, 0xD5, 0xF9, 0x97, 0x0F, 0x94,
+						0x5C, 0x77, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x80, // commitamt (set MSB for P2SH)
+						0x00, 0x58,
+					},
+					Value:   0,
+					Version: 0,
+				}, {
+					PkScript: []byte{
+						0xBD, 0x76, 0xA9, 0x14, 0x00, 0x00, 0x00, 0x00,
+						0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+						0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+						0x88, 0xAC,
+					},
+					Value:   0,
+					Version: 0,
+				}},
+			}
 			return chain
 		}(),
 		result: "0100000001395ebc9af44c4a696fa8e6287bdbf0a89a4d6207f191cb0f1eefc25" +
@@ -2222,10 +2220,8 @@ func TestHandleCreateRawSSRtx(t *testing.T) {
 				txType:     stake.TxTypeRegular,
 				height:     100000,
 				index:      0,
-				txVersion:  1,
 				isCoinBase: false,
 				hasExpiry:  true,
-				modified:   false,
 			}
 			return chain
 		}(),
@@ -2268,34 +2264,39 @@ func TestHandleCreateRawSSRtx(t *testing.T) {
 		},
 		mockChain: func() *testRPCChain {
 			chain := defaultMockRPCChain()
-			chain.convertUtxosToMinimalOutputs = []*stake.MinimalOutput{{
-				PkScript: []byte{
-					0xBA, 0xA9, 0x14, 0x78, 0x02, 0x39, 0xEA, 0x12,
-					0x31, 0xBA, 0x67, 0xB0, 0xC5, 0xB8, 0x2E, 0x78,
-					0x6B, 0x51, 0xE2, 0x10, 0x72, 0x52, 0x21, 0x87,
-				},
-				Value:   100000000,
-				Version: 0,
-			}, {
-				PkScript: []byte{
-					0x6A, 0x1E, 0x35, 0x5C, 0x96, 0xF4, 0x86, 0x12,
-					0xD5, 0x75, 0x09, 0x14, 0x0E, 0x9A, 0x04, 0x99,
-					0x81, 0xD5, 0xF9, 0x97, 0x0F, 0x94,
-					0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // commitamt (invalid amount)
-					0x00, 0x58,
-				},
-				Value:   0,
-				Version: 0,
-			}, {
-				PkScript: []byte{
-					0xBD, 0x76, 0xA9, 0x14, 0x00, 0x00, 0x00, 0x00,
-					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-					0x88, 0xAC,
-				},
-				Value:   0,
-				Version: 0,
-			}}
+			chain.fetchUtxoEntry = &testRPCUtxoEntry{
+				hasExpiry: true,
+				height:    100000,
+				txType:    stake.TxTypeSStx,
+				ticketMinimalOutputs: []*stake.MinimalOutput{{
+					PkScript: []byte{
+						0xBA, 0xA9, 0x14, 0x78, 0x02, 0x39, 0xEA, 0x12,
+						0x31, 0xBA, 0x67, 0xB0, 0xC5, 0xB8, 0x2E, 0x78,
+						0x6B, 0x51, 0xE2, 0x10, 0x72, 0x52, 0x21, 0x87,
+					},
+					Value:   100000000,
+					Version: 0,
+				}, {
+					PkScript: []byte{
+						0x6A, 0x1E, 0x35, 0x5C, 0x96, 0xF4, 0x86, 0x12,
+						0xD5, 0x75, 0x09, 0x14, 0x0E, 0x9A, 0x04, 0x99,
+						0x81, 0xD5, 0xF9, 0x97, 0x0F, 0x94,
+						0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // commitamt (invalid amount)
+						0x00, 0x58,
+					},
+					Value:   0,
+					Version: 0,
+				}, {
+					PkScript: []byte{
+						0xBD, 0x76, 0xA9, 0x14, 0x00, 0x00, 0x00, 0x00,
+						0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+						0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+						0x88, 0xAC,
+					},
+					Value:   0,
+					Version: 0,
+				}},
+			}
 			return chain
 		}(),
 		wantErr: true,
@@ -4868,13 +4869,12 @@ func TestHandleGetTxOut(t *testing.T) {
 	chainWithTx := func() *testRPCChain {
 		chain := defaultMockRPCChain()
 		chain.fetchUtxoEntry = &testRPCUtxoEntry{
-			amountByIndex:        txOut.Value,
-			height:               432100,
-			index:                1,
-			pkScriptByIndex:      script,
-			scriptVersionByIndex: scriptVersion,
-			txType:               stake.TxTypeRegular,
-			txVersion:            msgTx.Version,
+			amount:        txOut.Value,
+			height:        432100,
+			index:         1,
+			pkScript:      script,
+			scriptVersion: scriptVersion,
+			txType:        stake.TxTypeRegular,
 		}
 		return chain
 	}
@@ -4908,22 +4908,22 @@ func TestHandleGetTxOut(t *testing.T) {
 		}(),
 		result: nil,
 	}, {
-		name:    "handleGetTxOut: ok transaction exists but wrong tree",
+		name:    "handleGetTxOut: ok transaction exists in mempool but wrong tree",
 		handler: handleGetTxOut,
 		cmd:     &cmd,
 		mockChain: func() *testRPCChain {
 			chain := defaultMockRPCChain()
-			chain.fetchUtxoEntry = &testRPCUtxoEntry{
-				amountByIndex:        txOut.Value,
-				height:               432100,
-				index:                1,
-				pkScriptByIndex:      script,
-				scriptVersionByIndex: scriptVersion,
-				txType:               stake.TxTypeSStx,
-				txVersion:            msgTx.Version,
-			}
+			chain.fetchUtxoEntry = nil
 			return chain
 		}(),
+		mockTxMempooler: func() *testTxMempooler {
+			mp := defaultMockTxMempooler()
+			tx := dcrutil.NewTx(hexToMsgTx(txHex))
+			tx.SetTree(wire.TxTreeStake)
+			mp.fetchTransaction = tx
+			mp.fetchTransactionErr = nil
+			return mp
+		}(),
 		result: nil,
 	}, {
 		name:    "handleGetTxOut: invalid txid",
@@ -4966,6 +4966,7 @@ func TestHandleGetTxOut(t *testing.T) {
 		mockTxMempooler: func() *testTxMempooler {
 			mp := defaultMockTxMempooler()
 			tx := dcrutil.NewTx(hexToMsgTx(txHex))
+			tx.SetTree(wire.TxTreeRegular)
 			tx.MsgTx().TxOut[0] = nil
 			mp.fetchTransaction = tx
 			mp.fetchTransactionErr = nil