blockchain: Separate utxo state from tx flags.

This splits the utxo packed flags into two separate types, utxoState
and utxoFlags.  The reasoning is that:
- This cleanly separates the purpose of the flags.  utxoState defines
  the in-memory state of a utxo entry, whereas utxoFlags defines
  additional information for the containing transaction of a utxo
  entry.
- This makes room for an additional state that is required for the utxo
  cache, namely whether or not a utxo entry is fresh (does not exist as
  an unspent transaction output in the database).
This commit is contained in:
Ryan Staudt 2021-01-21 06:35:59 -06:00 committed by Dave Collins
parent 20d48dc775
commit 21b0dadcb3
5 changed files with 84 additions and 101 deletions

View File

@ -1,5 +1,5 @@
// Copyright (c) 2015-2016 The btcsuite developers
// Copyright (c) 2016-2020 The Decred developers
// Copyright (c) 2016-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -993,16 +993,13 @@ func deserializeUtxoEntry(serialized []byte, txOutIndex uint32) (*UtxoEntry, err
offset += bytesRead
// 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),
packedFlags: encodeUtxoFlags(isCoinBase, hasExpiry, txType),
}
// Copy the minimal outputs if this was a ticket submission output.

View File

@ -1,5 +1,5 @@
// Copyright (c) 2015-2016 The btcsuite developers
// Copyright (c) 2015-2020 The Decred developers
// Copyright (c) 2015-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@ -828,9 +828,6 @@ func TestUtxoSerialization(t *testing.T) {
withCoinbase = true
noExpiry = false
withExpiry = true
unspent = false
spent = true
unmodified = false
)
tests := []struct {
@ -851,8 +848,6 @@ func TestUtxoSerialization(t *testing.T) {
scriptVersion: 0,
packedFlags: encodeUtxoFlags(
withCoinbase,
unspent,
unmodified,
noExpiry,
stake.TxTypeRegular,
),
@ -872,8 +867,6 @@ func TestUtxoSerialization(t *testing.T) {
scriptVersion: 0,
packedFlags: encodeUtxoFlags(
withCoinbase,
unspent,
unmodified,
noExpiry,
stake.TxTypeRegular,
),
@ -892,8 +885,6 @@ func TestUtxoSerialization(t *testing.T) {
scriptVersion: 0,
packedFlags: encodeUtxoFlags(
noCoinbase,
unspent,
unmodified,
noExpiry,
stake.TxTypeRegular,
),
@ -912,8 +903,6 @@ func TestUtxoSerialization(t *testing.T) {
scriptVersion: 0,
packedFlags: encodeUtxoFlags(
noCoinbase,
unspent,
unmodified,
withExpiry,
stake.TxTypeSStx,
),
@ -940,8 +929,6 @@ func TestUtxoSerialization(t *testing.T) {
scriptVersion: 0xffff,
packedFlags: encodeUtxoFlags(
withCoinbase,
unspent,
unmodified,
noExpiry,
stake.TxTypeRegular,
),
@ -960,8 +947,6 @@ func TestUtxoSerialization(t *testing.T) {
scriptVersion: 0,
packedFlags: encodeUtxoFlags(
noCoinbase,
unspent,
unmodified,
withExpiry,
stake.TxTypeRegular,
),
@ -979,10 +964,9 @@ func TestUtxoSerialization(t *testing.T) {
blockHeight: 33333,
blockIndex: 3,
scriptVersion: 0,
state: utxoStateModified | utxoStateSpent,
packedFlags: encodeUtxoFlags(
withCoinbase,
spent,
unmodified,
withExpiry,
stake.TxTypeRegular,
),

View File

@ -6,55 +6,57 @@ package blockchain
import "github.com/decred/dcrd/blockchain/stake/v4"
// utxoFlags defines additional information and state for a transaction output
// in a utxo view. The bit representation is:
// utxoState defines the in-memory state of a utxo entry.
//
// The bit representation is:
// bit 0 - transaction output has been spent
// bit 1 - transaction output has been modified since it was loaded
// bits 2-7 - unused
type utxoState uint8
const (
// utxoStateSpent indicates that a txout is spent.
utxoStateSpent utxoState = 1 << iota
// utxoStateModified indicates that a txout has been modified since it was
// loaded.
utxoStateModified
)
// utxoFlags defines additional information for the containing transaction of a
// utxo entry.
//
// 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
// bit 1 - containing transaction has an expiry
// bits 2-5 - transaction type
type utxoFlags uint8
const (
// utxoFlagCoinBase indicates that a txout was contained in a coinbase tx.
utxoFlagCoinBase utxoFlags = 1 << iota
// utxoFlagSpent indicates that a txout is spent.
utxoFlagSpent
// utxoFlagModified indicates that a txout has been modified since it was
// loaded.
utxoFlagModified
// utxoFlagHasExpiry indicates that a txout was contained in a tx that
// included an expiry.
utxoFlagHasExpiry
)
const (
// utxoFlagTxTypeBitmask describes the bitmask that yields bits 4-7 from
// utxoFlagTxTypeBitmask describes the bitmask that yields bits 2-5 from
// utxoFlags.
utxoFlagTxTypeBitmask = 0xf0
utxoFlagTxTypeBitmask = 0x3c
// 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
utxoFlagTxTypeShift = 2
)
// encodeUtxoFlags returns utxoFlags representing the passed parameters.
func encodeUtxoFlags(coinbase bool, spent bool, modified bool, hasExpiry bool,
txType stake.TxType) utxoFlags {
func encodeUtxoFlags(coinbase 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
}
@ -100,16 +102,20 @@ type UtxoEntry struct {
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.
// state contains info for the in-memory state of the output as defined by
// utxoState.
state utxoState
// packedFlags contains additional info for the containing transaction of the
// output as defined by utxoFlags. This approach is used in order to reduce
// memory usage since there will be a lot of these in memory.
packedFlags utxoFlags
}
// isModified returns whether or not the output has been modified since it was
// loaded.
func (entry *UtxoEntry) isModified() bool {
return entry.packedFlags&utxoFlagModified == utxoFlagModified
return entry.state&utxoStateModified == utxoStateModified
}
// IsCoinBase returns whether or not the output was contained in a coinbase
@ -121,7 +127,7 @@ func (entry *UtxoEntry) IsCoinBase() 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.
func (entry *UtxoEntry) IsSpent() bool {
return entry.packedFlags&utxoFlagSpent == utxoFlagSpent
return entry.state&utxoStateSpent == utxoStateSpent
}
// HasExpiry returns whether or not the output was contained in a transaction
@ -157,7 +163,7 @@ func (entry *UtxoEntry) Spend() {
}
// Mark the output as spent and modified.
entry.packedFlags |= utxoFlagSpent | utxoFlagModified
entry.state |= utxoStateSpent | utxoStateModified
}
// Amount returns the amount of the output.
@ -203,6 +209,7 @@ func (entry *UtxoEntry) Clone() *UtxoEntry {
blockHeight: entry.blockHeight,
blockIndex: entry.blockIndex,
scriptVersion: entry.scriptVersion,
state: entry.state,
packedFlags: entry.packedFlags,
}

View File

@ -20,40 +20,31 @@ func TestEncodeUtxoFlags(t *testing.T) {
tests := []struct {
name string
coinbase bool
spent bool
modified bool
hasExpiry bool
txType stake.TxType
want utxoFlags
}{{
name: "no flags set, regular tx",
coinbase: false,
spent: false,
modified: false,
hasExpiry: false,
txType: stake.TxTypeRegular,
want: 0x00,
}, {
name: "coinbase, has expiry, vote tx",
coinbase: true,
spent: false,
modified: false,
hasExpiry: true,
txType: stake.TxTypeSSGen,
want: 0x29,
want: 0x0b,
}, {
name: "spent, modified, has expiry, ticket tx",
name: "has expiry, ticket tx",
coinbase: false,
spent: true,
modified: true,
hasExpiry: true,
txType: stake.TxTypeSStx,
want: 0x1e,
want: 0x06,
}}
for _, test := range tests {
got := encodeUtxoFlags(test.coinbase, test.spent, test.modified,
test.hasExpiry, test.txType)
got := encodeUtxoFlags(test.coinbase, test.hasExpiry, test.txType)
if got != test.want {
t.Errorf("%q: unexpected result -- got %x, want %x", test.name, got,
test.want)
@ -104,9 +95,9 @@ func TestUtxoEntry(t *testing.T) {
tests := []struct {
name string
coinbase bool
spent bool
modified bool
coinbase bool
expiry bool
txType stake.TxType
amount int64
@ -171,13 +162,26 @@ func TestUtxoEntry(t *testing.T) {
scriptVersion: test.scriptVersion,
packedFlags: encodeUtxoFlags(
test.coinbase,
test.spent,
test.modified,
test.expiry,
test.txType,
),
}
// Set state flags given the parameters for the current test.
if test.spent {
entry.state |= utxoStateSpent
}
if test.modified {
entry.state |= utxoStateModified
}
// Validate the spent flag.
isSpent := entry.IsSpent()
if isSpent != test.spent {
t.Fatalf("%q: unexpected spent flag -- got %v, want %v", test.name,
isSpent, test.spent)
}
// Validate the modified flag.
isModified := entry.isModified()
if isModified != test.modified {
@ -192,13 +196,6 @@ func TestUtxoEntry(t *testing.T) {
isCoinBase, test.coinbase)
}
// Validate the spent flag.
isSpent := entry.IsSpent()
if isSpent != test.spent {
t.Fatalf("%q: unexpected spent flag -- got %v, want %v", test.name,
isSpent, test.spent)
}
// Validate the expiry flag.
hasExpiry := entry.HasExpiry()
if hasExpiry != test.expiry {
@ -206,6 +203,13 @@ func TestUtxoEntry(t *testing.T) {
hasExpiry, test.expiry)
}
// Validate the type of the transaction that the output is contained in.
gotTxType := entry.TransactionType()
if gotTxType != test.txType {
t.Fatalf("%q: unexpected transaction type -- got %v, want %v", test.name,
gotTxType, test.txType)
}
// Validate the height of the block containing the output.
gotBlockHeight := entry.BlockHeight()
if gotBlockHeight != int64(test.blockHeight) {
@ -220,13 +224,6 @@ func TestUtxoEntry(t *testing.T) {
gotBlockIndex, test.blockIndex)
}
// Validate the type of the transaction that the output is contained in.
gotTxType := entry.TransactionType()
if gotTxType != test.txType {
t.Fatalf("%q: unexpected transaction type -- got %v, want %v", test.name,
gotTxType, test.txType)
}
// Validate the amount of the output.
gotAmount := entry.Amount()
if gotAmount != test.amount {

View File

@ -79,6 +79,11 @@ func (view *UtxoViewpoint) addTxOut(outpoint wire.OutPoint, txOut *wire.TxOut,
entry.scriptVersion = txOut.Version
entry.packedFlags = packedFlags
entry.ticketMinOuts = ticketMinOuts
// The referenced transaction output should always be marked as unspent and
// modified when being added to the view.
entry.state &^= utxoStateSpent
entry.state |= utxoStateModified
}
// AddTxOut adds the specified output of the passed transaction to the view if
@ -96,15 +101,13 @@ func (view *UtxoViewpoint) AddTxOut(tx *dcrutil.Tx, txOutIdx uint32,
// 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)
flags := encodeUtxoFlags(isCoinBase, hasExpiry, txType)
// Update existing entries. All fields are updated because it's possible
// (although extremely unlikely) that the existing entry is being replaced by
@ -131,15 +134,13 @@ func (view *UtxoViewpoint) AddTxOuts(tx *dcrutil.Tx, blockHeight int64, blockInd
// 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)
flags := encodeUtxoFlags(isCoinBase, hasExpiry, txType)
// Loop through all of the transaction outputs and add those which are not
// provably unspendable.
@ -325,16 +326,14 @@ func (view *UtxoViewpoint) disconnectTransactions(block *dcrutil.Block, stxos []
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),
state: utxoStateModified,
packedFlags: encodeUtxoFlags(isCoinBase, hasExpiry, txType),
}
if isTicketSubmissionOutput(txType, uint32(txOutIdx)) {
@ -374,8 +373,6 @@ func (view *UtxoViewpoint) disconnectTransactions(block *dcrutil.Block, stxos []
txIn := msgTx.TxIn[txInIdx]
entry := view.entries[txIn.PreviousOutPoint]
if entry == nil {
const spent = false
const modified = true
entry = &UtxoEntry{
amount: txIn.ValueIn,
pkScript: stxo.pkScript,
@ -383,8 +380,9 @@ func (view *UtxoViewpoint) disconnectTransactions(block *dcrutil.Block, stxos []
blockHeight: stxo.blockHeight,
blockIndex: stxo.blockIndex,
scriptVersion: stxo.scriptVersion,
packedFlags: encodeUtxoFlags(stxo.IsCoinBase(), spent, modified,
stxo.HasExpiry(), stxo.TransactionType()),
state: utxoStateModified,
packedFlags: encodeUtxoFlags(stxo.IsCoinBase(), stxo.HasExpiry(),
stxo.TransactionType()),
}
view.entries[txIn.PreviousOutPoint] = entry
@ -392,8 +390,8 @@ func (view *UtxoViewpoint) disconnectTransactions(block *dcrutil.Block, stxos []
// Mark the existing referenced transaction output as unspent and
// modified.
entry.packedFlags &^= utxoFlagSpent
entry.packedFlags |= utxoFlagModified
entry.state &^= utxoStateSpent
entry.state |= utxoStateModified
}
}
@ -601,7 +599,7 @@ func (view *UtxoViewpoint) commit() {
continue
}
entry.packedFlags &^= utxoFlagModified
entry.state &^= utxoStateModified
}
}