dcrd/internal/mining/mining_view_test.go
Dave Collins ad02d8f300
multi: Go 1.19 doc comment formatting.
This modifies the entire repository to use the new formatting of doc
comments in the upcoming Go 1.19 release.

The primary motivating factors for this are:

- Builds check that files are formatted per gofmt and that will no
  longer be true as of Go 1.19 without these changes
- Separating all the updates into a single commit ensures these
  documentation only formatting changes do not clutter up diffs that
  actually change code

For the most part, the changes are just the automated changes suggested
by the Go 1.19 version of gofmt, but there are also a few cases where
the comments were reworded a bit to play nicely with the new formatting
requirements.

For example, the new version of gofmt reformats and collapses nested
lists where as the existing version does not.  Thus, instances of nested
lists have been changed to either eliminate them or use mixed markers
which produce expect results.
2022-07-30 04:08:58 -05:00

522 lines
16 KiB
Go

// Copyright (c) 2020-2022 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package mining
import (
"testing"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/wire"
)
// Tests the behavior of the mining view when returned from a tx source
// containing a transaction chain depicted as:
//
// /--> d --> f
// |
// |--> b --|
// <coinbase> --> a |-->e
// |--> c --|
func TestMiningView(t *testing.T) {
harness, spendableOuts, err := newMiningHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("unable to create mining harness: %v", err)
}
applyTxFee := func(fee int64) func(*wire.MsgTx) {
return func(tx *wire.MsgTx) {
tx.TxOut[0].Value -= fee
}
}
// All transactions have the same number of outputs to keep the size uniform.
txA, _ := harness.CreateSignedTx([]spendableOutput{
spendableOuts[0],
}, 2, applyTxFee(1000))
txB, _ := harness.CreateSignedTx([]spendableOutput{
txOutToSpendableOut(txA, 0, wire.TxTreeRegular),
}, 2, applyTxFee(5000))
txC, _ := harness.CreateSignedTx([]spendableOutput{
txOutToSpendableOut(txA, 1, wire.TxTreeRegular),
}, 2, applyTxFee(5000))
txD, _ := harness.CreateSignedTx([]spendableOutput{
txOutToSpendableOut(txB, 0, wire.TxTreeRegular),
}, 2, applyTxFee(5000))
txE, _ := harness.CreateSignedTx([]spendableOutput{
txOutToSpendableOut(txB, 1, wire.TxTreeRegular),
txOutToSpendableOut(txC, 0, wire.TxTreeRegular),
}, 2, applyTxFee(5000))
txF, _ := harness.CreateSignedTx([]spendableOutput{
txOutToSpendableOut(txD, 0, wire.TxTreeRegular),
}, 2, applyTxFee(0))
// Add all to the tx source. Note that txB is out of order to test the
// behavior of an orphan transaction with ancestors brought into the tx source.
allTxns := []*dcrutil.Tx{txB, txA, txC, txD, txE}
for _, tx := range allTxns {
_, err = harness.AddTransactionToTxSource(tx)
if err != nil {
t.Fatalf("unable to add transaction to tx source: %v", err)
}
}
txALen := int64(txA.MsgTx().SerializeSize())
txBLen := int64(txB.MsgTx().SerializeSize())
txCLen := int64(txC.MsgTx().SerializeSize())
txATotalSigOps, err := harness.CountTotalSigOps(txA)
if err != nil {
t.Fatalf("failed to count sigops for txA %v", err)
}
txBTotalSigOps, err := harness.CountTotalSigOps(txB)
if err != nil {
t.Fatalf("failed to count sigops for txB %v", err)
}
txCTotalSigOps, err := harness.CountTotalSigOps(txC)
if err != nil {
t.Fatalf("failed to count sigops for txC %v", err)
}
tests := []struct {
name string
miningView TxMiningView
subject *dcrutil.Tx
expectedAncestorFees int64
expectedSizeBytes int64
expectedSigOps int
ancestors []*dcrutil.Tx
descendants map[chainhash.Hash]*dcrutil.Tx
orderedAncestors [][]*dcrutil.Tx
}{{
name: "Tx A",
subject: txA,
expectedAncestorFees: 0,
expectedSizeBytes: 0,
expectedSigOps: 0,
ancestors: nil,
descendants: map[chainhash.Hash]*dcrutil.Tx{
txB.MsgTx().TxHash(): txB,
txC.MsgTx().TxHash(): txC,
txD.MsgTx().TxHash(): txD,
txE.MsgTx().TxHash(): txE,
},
orderedAncestors: nil,
}, {
name: "Tx B",
subject: txB,
expectedAncestorFees: 1000,
expectedSizeBytes: txALen,
expectedSigOps: txATotalSigOps,
ancestors: []*dcrutil.Tx{txA},
descendants: map[chainhash.Hash]*dcrutil.Tx{
txD.MsgTx().TxHash(): txD,
txE.MsgTx().TxHash(): txE,
},
orderedAncestors: [][]*dcrutil.Tx{
{txA},
},
}, {
name: "Tx C",
subject: txC,
expectedAncestorFees: 1000,
expectedSizeBytes: txALen,
expectedSigOps: txATotalSigOps,
ancestors: []*dcrutil.Tx{txA},
descendants: map[chainhash.Hash]*dcrutil.Tx{
txE.MsgTx().TxHash(): txE,
},
orderedAncestors: [][]*dcrutil.Tx{
{txA},
},
}, {
name: "Tx D",
subject: txD,
expectedAncestorFees: 6000,
expectedSizeBytes: txALen + txBLen,
expectedSigOps: txATotalSigOps + txBTotalSigOps,
ancestors: []*dcrutil.Tx{txA, txB},
descendants: map[chainhash.Hash]*dcrutil.Tx{},
orderedAncestors: [][]*dcrutil.Tx{
{txA, txB},
},
}, {
name: "Tx E",
subject: txE,
expectedAncestorFees: 11000,
expectedSizeBytes: txALen + txBLen + txCLen,
expectedSigOps: txATotalSigOps + txBTotalSigOps +
txCTotalSigOps,
ancestors: []*dcrutil.Tx{txA, txB, txC},
descendants: map[chainhash.Hash]*dcrutil.Tx{},
orderedAncestors: [][]*dcrutil.Tx{
{txA, txB, txC},
{txA, txC, txB},
},
}}
for _, test := range tests {
txHash := test.subject.Hash()
txDesc := harness.txSource.pool[*txHash]
miningView := harness.txSource.MiningView()
// Make sure transaction is not rejected between view instances.
if miningView.isRejected(txHash) {
t.Fatalf("%v: expected test subject txn %v to not be rejected",
test.name, *txHash)
}
// Ensure stats are calculated before retrieving ancestors.
initialStat, hasStats := miningView.AncestorStats(txHash)
if !hasStats {
t.Fatalf("%v: expected mining view to have ancestor stats",
test.name)
}
if test.expectedAncestorFees != initialStat.Fees {
t.Fatalf("%v: expected subject txn to have bundle fees %v, got %v",
test.name, test.expectedAncestorFees, initialStat.Fees)
}
if test.expectedSizeBytes != initialStat.SizeBytes {
t.Fatalf("%v: expected subject txn to have SizeBytes=%v, got %v",
test.name, test.expectedSizeBytes, initialStat.SizeBytes)
}
if test.expectedSigOps != initialStat.TotalSigOps {
t.Fatalf("%v: expected subject txn to have SigOps=%v, got %v",
test.name, test.expectedSigOps, initialStat.TotalSigOps)
}
if len(test.ancestors) != initialStat.NumAncestors {
t.Fatalf("%v: expected subject txn to have NumAncestors=%v, got %v",
test.name, len(test.ancestors), initialStat.NumAncestors)
}
// Retrieving ancestors will cause the cached stats to be updated.
ancestors := miningView.ancestors(txHash)
stat, _ := miningView.AncestorStats(txHash)
if initialStat == stat {
t.Fatalf("%v: expected ancestor stats reference to have changed "+
"after retrieving list of ancestors", test.name)
}
// Get snapshot of transaction relationships as they exist in the tx source.
descendants := harness.txSource.miningView.descendants(txHash)
if len(test.ancestors) != len(ancestors) {
t.Fatalf("%v: expected subject txn to have %v ancestors, got %v",
test.name, len(test.ancestors), len(ancestors))
}
if len(ancestors) != stat.NumAncestors {
t.Fatalf("%v: expected subject txn to have NumAncestors=%v, got %v",
test.name, len(ancestors), stat.NumAncestors)
}
if len(descendants) != stat.NumDescendants {
t.Fatalf("%v: expected subject txn to have NumDescendants=%v, "+
"got %v", test.name, len(descendants), stat.NumDescendants)
}
for _, descendantHash := range descendants {
if _, exist := test.descendants[*descendantHash]; !exist {
t.Fatalf("%v: expected subject txn to have descendant=%v",
test.name, descendantHash)
}
}
// Ensure that transactions have a valid order, and that one of the test's
// orderedAncestors matches ancestors returned from the view exactly.
exactMatch := true
for _, ancestorGroups := range test.orderedAncestors {
exactMatch = true
for index, ancestor := range ancestorGroups {
if *ancestors[index].Tx.Hash() != *ancestor.Hash() {
exactMatch = false
break
}
}
if exactMatch {
break
}
}
if !exactMatch {
t.Fatalf("%v: subject txn ancestors returned out of order",
test.name)
}
if miningView.hasParents(txHash) && len(test.ancestors) == 0 {
t.Fatalf("%v: expected subject txn to not have 0 parents, got %v",
test.name, len(test.ancestors))
}
// Make sure all parents are valid outpoints from this tx.
txInputHashes := make(map[chainhash.Hash]struct{})
for _, outpoint := range test.subject.MsgTx().TxIn {
txInputHashes[outpoint.PreviousOutPoint.Hash] = struct{}{}
}
for _, parentDesc := range miningView.parents(txHash) {
if _, exist := txInputHashes[*parentDesc.Tx.Hash()]; !exist {
t.Fatalf("%v: test subject %v has invalid parent %v",
test.name, *txHash, *parentDesc.Tx.Hash())
}
}
// if the transaction has children, make sure it's no longer the child's
// ancestor after removal.
removalMiningView := harness.txSource.MiningView()
if children := removalMiningView.children(txHash); len(children) > 0 {
removalMiningView.RemoveTransaction(txHash, false)
child := children[0]
siblings := removalMiningView.parents(child.Tx.Hash())
// Make sure ancestor stats have not changed.
oldStat, hasStats := miningView.AncestorStats(child.Tx.Hash())
if !hasStats {
t.Fatalf("%v: Expected ancestor stats to be available",
test.name)
}
newStat, hasStats := removalMiningView.AncestorStats(
child.Tx.Hash())
if !hasStats {
t.Fatalf("%v: Expected ancestor stats to be available",
test.name)
}
if *oldStat != *newStat {
t.Fatalf("%v: expected test subject's child bundle stats to "+
"remain unchanged.", test.name)
}
for _, sibling := range siblings {
if *sibling.Tx.Hash() == *txHash {
t.Fatalf("%v: expected test subject to no longer be an "+
"ancestor of %v after being removed from the view.",
test.name, *sibling.Tx.Hash())
}
}
}
// reset the mining view after removing the tx and ensure that all descendants
// have the fee removed for this transaction when calling Remove with the
// option to do so.
removalMiningView = harness.txSource.MiningView()
removalMiningView.RemoveTransaction(txHash, true)
for _, descendant := range descendants {
oldStat, hasStats := miningView.AncestorStats(descendant)
if !hasStats {
t.Fatalf("%v: Expected ancestor stats to be available",
test.name)
}
newStat, hasStats := removalMiningView.AncestorStats(descendant)
if !hasStats {
t.Fatalf("%v: Expected ancestor stats to be available",
test.name)
}
expectedFee := oldStat.Fees - txDesc.Fee
expectedSigOps := oldStat.TotalSigOps - txDesc.TotalSigOps
expectedSizeBytes := oldStat.SizeBytes -
int64(txDesc.Tx.MsgTx().SerializeSize())
expectedNumAncestors := oldStat.NumAncestors - 1
if newStat.Fees != expectedFee {
t.Fatalf("%v: expected descendant to have adjusted "+
"Fees=%v, but got %v", test.name, expectedFee, newStat.Fees)
}
if newStat.TotalSigOps != expectedSigOps {
t.Fatalf("%v: expected descendant to have adjusted "+
"NumSigOps=%v, but got %v", test.name,
expectedSigOps, newStat.TotalSigOps)
}
if newStat.SizeBytes != expectedSizeBytes {
t.Fatalf("%v: expected descendant to have adjusted "+
"SizeBytes=%v, but got %v", test.name,
expectedSizeBytes, newStat.SizeBytes)
}
if newStat.NumAncestors != expectedNumAncestors {
t.Fatalf("%v: expected descendant to have "+
"NumAncestors=%v, but got %v", test.name,
expectedNumAncestors, newStat.NumAncestors)
}
}
miningView.reject(txHash)
if !miningView.isRejected(txHash) {
t.Fatalf("%v: expected test subject txn %v to be rejected",
test.name, *txHash)
}
// Rejecting a transaction rejects all descendant transactions and removes the
// tx from the graph.
if ancestors := miningView.ancestors(txHash); len(ancestors) > 0 {
t.Fatalf("%v: expected subject txn to have 0 ancestors, got %v",
test.name, len(ancestors))
}
for _, descendant := range descendants {
if !miningView.isRejected(descendant) {
t.Fatalf("%v: expected descendant txn %v to be rejected.",
test.name, *descendant)
}
}
}
// Get mining view snapshot prior to removing the tx from the tx source.
miningViewSnapshot := harness.txSource.MiningView()
// Remove txC and its descendants from the tx source.
harness.RemoveTransactionFromTxSource(txC, true)
// Add a new transaction to the tx source.
_, err = harness.AddTransactionToTxSource(txF)
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
miningView := harness.txSource.MiningView()
addedTxns := miningView.children(txD.Hash())
if len(addedTxns) != 1 {
t.Fatalf("AddTxnToTxSource: expected txD to have exactly 1 child, got %v",
len(addedTxns))
}
if addedTxns[0].Tx != txF {
t.Fatalf("AddTxnToTxSource: expected txF to exist in the mining view.")
}
// Ensure newly added transaction not added to the mining view snapshot.
if len(miningViewSnapshot.children(txD.Hash())) != 0 {
t.Fatalf("AddTxnToTxSource: expected txD to not have descendants in " +
"snapshot mining view.")
}
if len(miningView.children(txC.Hash())) != 0 {
t.Fatalf("RemoveTxnFromTxSource: expected txC to not have descendents " +
"after being removed from the tx source.")
}
// Ensure tx and children are still accessible in cloned view.
if len(miningViewSnapshot.children(txC.Hash())) != 1 {
t.Fatalf("RemoveTxnFromTxSource: expected txC to exist in snapshot view " +
"even though it was removed from the tx source.")
}
}
// TestAncestorTrackingLimits ensures that ancestor tracking is disabled for
// transactions that have too many ancestors.
func TestAncestorTrackingLimits(t *testing.T) {
harness, spendableOuts, err := newMiningHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("unable to create mining harness: %v", err)
}
// Create a chain of transactions.
var allTxns []*dcrutil.Tx
prevSpendableOut := spendableOuts[0]
for i := 0; i < ancestorTrackingLimit+2; i++ {
tx, _ := harness.CreateSignedTx([]spendableOutput{
prevSpendableOut,
}, 1)
allTxns = append(allTxns, tx)
prevSpendableOut = txOutToSpendableOut(tx, 0, wire.TxTreeRegular)
}
// Add all transactions to the tx source.
for _, tx := range allTxns {
_, err = harness.AddTransactionToTxSource(tx)
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
}
// Remove each transaction from the tx source to demonstrate that once a
// transaction has less ancestors than the limit that it begins having ancestor
// stats tracked and it continues to do so until it is removed from the tx
// source.
allTxnsQueue := allTxns
for len(allTxnsQueue) > 0 {
for index, tx := range allTxnsQueue {
_, hasStats := harness.txSource.miningView.AncestorStats(tx.Hash())
if index <= ancestorTrackingLimit && !hasStats {
t.Fatalf("expected transaction %v at index %v to have ancestor"+
" tracking enabled", tx.Hash(), index)
}
if index > ancestorTrackingLimit && hasStats {
t.Fatalf("expected transaction %v at index %v to not have "+
"ancestor tracking enabled", tx.Hash(), index)
}
}
tx := allTxnsQueue[0]
allTxnsQueue = allTxnsQueue[1:]
// Simulate the transaction being included in a block.
newBlockHeight := harness.chain.bestState.Height + 1
harness.AddFakeUTXO(tx, newBlockHeight, wire.NullBlockIndex,
harness.chain.isTreasuryAgendaActive)
harness.chain.bestState.Height = newBlockHeight
harness.RemoveTransactionFromTxSource(tx, false)
harness.txSource.MaybeAcceptDependents(tx)
}
// Add all transactions back to the tx source in reverse order to demonstrate
// that a transaction that initially has ancestor stats tracked is later
// excluded from the ancestor tracked set when it has too many ancestors.
for index := len(allTxns) - 1; index >= 0; index-- {
tx := allTxns[index]
txHash := tx.Hash()
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)
}
_, hasStats := harness.txSource.miningView.AncestorStats(txHash)
if !hasStats {
t.Fatalf("expected transaction %v at index %v to have ancestor"+
" tracking enabled when added back to the tx source", txHash, index)
}
}
for index, tx := range allTxns {
txHash := tx.Hash()
_, hasStats := harness.txSource.miningView.AncestorStats(txHash)
if index <= ancestorTrackingLimit && !hasStats {
t.Fatalf("expected transaction %v at index %v to have ancestor"+
" tracking enabled", txHash, index)
}
if index > ancestorTrackingLimit && hasStats {
t.Fatalf("expected transaction %v at index %v to not have "+
"ancestor tracking enabled", txHash, index)
}
}
}