From 21b0dadcb3d770dc86e72fa600b1494e14a32c97 Mon Sep 17 00:00:00 2001 From: Ryan Staudt Date: Thu, 21 Jan 2021 06:35:59 -0600 Subject: [PATCH] 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). --- blockchain/chainio.go | 7 ++-- blockchain/chainio_test.go | 20 ++--------- blockchain/utxoentry.go | 69 ++++++++++++++++++++---------------- blockchain/utxoentry_test.go | 57 ++++++++++++++--------------- blockchain/utxoviewpoint.go | 32 ++++++++--------- 5 files changed, 84 insertions(+), 101 deletions(-) diff --git a/blockchain/chainio.go b/blockchain/chainio.go index 70ab0367..3dbe9ddf 100644 --- a/blockchain/chainio.go +++ b/blockchain/chainio.go @@ -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. diff --git a/blockchain/chainio_test.go b/blockchain/chainio_test.go index c83a66e6..8dffdde0 100644 --- a/blockchain/chainio_test.go +++ b/blockchain/chainio_test.go @@ -1,5 +1,5 @@ // Copyright (c) 2015-2016 The btcsuite developers -// Copyright (c) 2015-2020 The Decred developers +// Copyright (c) 2015-2021 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -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, ), diff --git a/blockchain/utxoentry.go b/blockchain/utxoentry.go index 98b1a664..43ecb394 100644 --- a/blockchain/utxoentry.go +++ b/blockchain/utxoentry.go @@ -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, } diff --git a/blockchain/utxoentry_test.go b/blockchain/utxoentry_test.go index 77ff3570..053149cb 100644 --- a/blockchain/utxoentry_test.go +++ b/blockchain/utxoentry_test.go @@ -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 { diff --git a/blockchain/utxoviewpoint.go b/blockchain/utxoviewpoint.go index caa767c0..629dda5f 100644 --- a/blockchain/utxoviewpoint.go +++ b/blockchain/utxoviewpoint.go @@ -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 } }