dcrd/internal/mining/mining_test.go
Dave Collins 8d1038cc45
blockchain: Start v5 module dev cycle.
Upcoming changes constitute breaking public API changes to the
blockchain module, therefore, this follows the process for introducing
major API breaks which consists of:

- Bump the major version in the go.mod of the affected module if not
  already done since the last release tag
- Add a replacement to the go.mod in the main module if not already done
  since the last release tag
- Update all imports in the repo to use the new major version as
  necessary
- Make necessary modifications to allow all other modules to use the new
  version in the same commit
- Repeat the process for any other modules the require a new major as a
  result of consuming the new major(s)
2022-03-24 12:44:05 -05:00

674 lines
22 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 (
"encoding/binary"
"errors"
"reflect"
"testing"
"github.com/decred/dcrd/blockchain/v5"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/txscript/v4"
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/wire"
)
// TestNewBlockTemplateBasicErrorScenarios tests various basic error scenarios
// that can occur during new block template generation.
func TestNewBlockTemplateBasicErrorScenarios(t *testing.T) {
t.Parallel()
// Create a new mining harness instance.
harness, _, err := newMiningHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("error creating mining harness: %v", err)
}
// Create a test address for use in template generation.
address, err := stdaddr.DecodeAddress("Dsi8CRt85xYyempXs7ZPL1rBxvDdAGZmgsg",
harness.chainParams)
if err != nil {
t.Fatalf("error decoding address: %v", err)
}
// Test error retrieving standard verify flags.
standardVerifyFlags := harness.policy.StandardVerifyFlags
var errFlags = errors.New("error retrieving standard verify flags")
harness.policy.StandardVerifyFlags = func() (txscript.ScriptFlags, error) {
return 0, errFlags
}
_, err = harness.generator.NewBlockTemplate(address)
if !errors.Is(err, errFlags) {
t.Fatalf("unexpected error retrieving standard verify flags -- got %v, "+
"want %v", err, errFlags)
}
harness.policy.StandardVerifyFlags = standardVerifyFlags
// Test error retrieving treasury agenda.
var errTreasuryAgenda = errors.New("error retrieving treasury agenda")
harness.chain.isTreasuryAgendaActiveErr = errTreasuryAgenda
_, err = harness.generator.NewBlockTemplate(address)
if !errors.Is(err, errTreasuryAgenda) {
t.Fatalf("unexpected error retrieving treasury agenda -- got %v, want %v",
err, errTreasuryAgenda)
}
harness.chain.isTreasuryAgendaActiveErr = nil
}
// TestNewBlockTemplate tests the generation of a new block template containing
// regular and vote transactions.
func TestNewBlockTemplate(t *testing.T) {
t.Parallel()
// Create a new mining harness instance.
harness, spendableOuts, err := newMiningHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("error creating mining harness: %v", err)
}
// Create a test address for use in template generation.
address, err := stdaddr.DecodeAddress("Dsi8CRt85xYyempXs7ZPL1rBxvDdAGZmgsg",
harness.chainParams)
if err != nil {
t.Fatalf("error decoding address: %v", err)
}
// Define a munger to apply transaction fees.
applyTxFee := func(fee int64) func(*wire.MsgTx) {
return func(tx *wire.MsgTx) {
tx.TxOut[0].Value -= fee
}
}
// Create additional transactions from the first spendable output provided by
// the harness.
const numTxs = 6
txs := make([]*dcrutil.Tx, numTxs)
baseTx, err := harness.CreateSignedTx(spendableOuts, uint32(numTxs))
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
harness.AddFakeUTXO(baseTx, harness.chain.bestState.Height, 1,
harness.chain.isTreasuryAgendaActive)
for i := 0; i < numTxs; i++ {
tx, err := harness.CreateSignedTx([]spendableOutput{
txOutToSpendableOut(baseTx, uint32(i), wire.TxTreeRegular)}, 1,
applyTxFee(5000))
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
txs[i] = tx
}
// Create ticket purchase transactions spending the outputs of the prior
// regular transactions.
const numVotes = 5
tickets := make([]*dcrutil.Tx, numVotes)
ticketHashes := make([]chainhash.Hash, numVotes)
for i := 0; i < numVotes; i++ {
ticket, err := harness.CreateTicketPurchase(txs[i], 40000)
if err != nil {
t.Fatalf("unable to create ticket purchase transaction: %v", err)
}
tickets[i] = ticket
ticketHashes[i] = ticket.MsgTx().TxHash()
}
// Add the ticket outputs as utxos to fake their existence. Use one after
// the stake enabled height for the height of the fake utxos to ensure they
// are mature for the votes cast at stake validation height below.
harness.chain.bestState.Height = harness.chainParams.StakeEnabledHeight + 1
for i, ticket := range tickets {
harness.AddFakeUTXO(ticket, harness.chain.bestState.Height, uint32(i+1),
harness.chain.isTreasuryAgendaActive)
}
// Create votes on a block at stake validation height using the previously
// created tickets.
harness.chain.bestState.Height = harness.chainParams.StakeValidationHeight
harness.chain.bestState.NextWinningTickets = ticketHashes
votes := make([]*dcrutil.Tx, numVotes)
for i, ticket := range tickets {
vote, err := harness.CreateVote(ticket)
if err != nil {
t.Fatalf("unable to create vote: %v", err)
}
votes[i] = vote
}
// Add vote transactions to the tx source.
for _, vote := range votes {
_, err = harness.AddTransactionToTxSource(vote)
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
}
// Add remaining regular transactions to the tx source.
for i := numVotes; i < numTxs; i++ {
_, err = harness.AddTransactionToTxSource(txs[i])
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
}
// Generate a new block template.
blockTemplate, err := harness.generator.NewBlockTemplate(address)
if err != nil {
t.Fatalf("unexpected err generating block template: %v", err)
}
// Validate the number of transactions in the generated block template.
gotTx := len(blockTemplate.Block.Transactions)
wantTx := numTxs - numVotes + 1 // + 1 for coinbase.
if gotTx != wantTx {
t.Fatalf("unexpected number of transactions in template -- got %v, want %v",
gotTx, wantTx)
}
// Validate the number of stake transactions in the generated block template.
gotStx := len(blockTemplate.Block.STransactions)
wantStx := numVotes + 1 // + 1 for stakebase.
if gotStx != wantStx {
t.Fatalf("unexpected number of stake transactions in template -- got %v, "+
"want %v", gotStx, wantStx)
}
// Validate that the block is sane. These checks are context free.
block := dcrutil.NewBlock(blockTemplate.Block)
err = blockchain.CheckBlockSanity(block, harness.generator.cfg.TimeSource,
harness.chainParams)
if err != nil {
t.Fatalf("unexpected error when checking block sanity: %v", err)
}
}
// TestNewBlockTemplateAutoRevocations tests the generation of a new block with
// automatic ticket revocations enabled.
func TestNewBlockTemplateAutoRevocations(t *testing.T) {
t.Parallel()
// Create a new mining harness instance.
harness, spendableOuts, err := newMiningHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("error creating mining harness: %v", err)
}
// Activate the automatic ticket revocations agenda.
harness.chain.isAutoRevocationsAgendaActive = true
// Create a test address for use in template generation.
address, err := stdaddr.DecodeAddress("Dsi8CRt85xYyempXs7ZPL1rBxvDdAGZmgsg",
harness.chainParams)
if err != nil {
t.Fatalf("error decoding address: %v", err)
}
// Define a munger to apply transaction fees.
applyTxFee := func(fee int64) func(*wire.MsgTx) {
return func(tx *wire.MsgTx) {
tx.TxOut[0].Value -= fee
}
}
// Create additional transactions from the first spendable output provided by
// the harness.
const numTxs = 11
txs := make([]*dcrutil.Tx, numTxs)
baseTx, err := harness.CreateSignedTx(spendableOuts, uint32(numTxs))
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
harness.AddFakeUTXO(baseTx, harness.chain.bestState.Height, 1,
harness.chain.isTreasuryAgendaActive)
for i := 0; i < numTxs; i++ {
tx, err := harness.CreateSignedTx([]spendableOutput{
txOutToSpendableOut(baseTx, uint32(i), wire.TxTreeRegular)}, 1,
applyTxFee(5000))
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
txs[i] = tx
}
// Create ticket purchase transactions spending the outputs of the prior
// regular transactions.
const numTickets = 10
tickets := make([]*dcrutil.Tx, numTickets)
ticketHashes := make([]chainhash.Hash, numTickets)
for i := 0; i < numTickets; i++ {
ticket, err := harness.CreateTicketPurchase(txs[i], 40000)
if err != nil {
t.Fatalf("unable to create ticket purchase transaction: %v", err)
}
tickets[i] = ticket
ticketHashes[i] = ticket.MsgTx().TxHash()
}
// Add the ticket outputs as utxos to fake their existence. Use one after
// the stake enabled height for the height of the fake utxos to ensure they
// are mature for the votes cast at stake validation height below.
harness.chain.bestState.Height = harness.chainParams.StakeEnabledHeight + 1
for i, ticket := range tickets {
harness.AddFakeUTXO(ticket, harness.chain.bestState.Height, uint32(i+1),
harness.chain.isTreasuryAgendaActive)
}
// Create votes on a block at stake validation height using the previously
// created tickets.
//
// Create votes for 3 out of 5 winning tickets. The 2 remaining winning
// tickets will be missed and will need to be revoked.
winningTickets := ticketHashes[:5]
voteTickets := winningTickets[:3]
missedThisBlock := winningTickets[3:]
harness.chain.bestState.Height = harness.chainParams.StakeValidationHeight
harness.chain.bestState.NextWinningTickets = winningTickets
numVotes := len(voteTickets)
votes := make([]*dcrutil.Tx, numVotes)
for i, ticket := range tickets[:numVotes] {
vote, err := harness.CreateVote(ticket)
if err != nil {
t.Fatalf("unable to create vote: %v", err)
}
votes[i] = vote
}
// Add vote transactions to the tx source.
for _, vote := range votes {
_, err = harness.AddTransactionToTxSource(vote)
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
}
// Add 2 of the remaining non-winning tickets to the list of tickets expiring
// next block to simulate expiring tickets that will need to be revoked.
expiredThisBlock := ticketHashes[5:7]
harness.chain.bestState.NextExpiringTickets = expiredThisBlock
// Add 2 of the remaining non-winning tickets to the list of tickets that were
// already missed.
missedPreviously := ticketHashes[7:9]
harness.chain.bestState.MissedTickets = missedPreviously
// Use 1 ticket as a ticket that is ineligible to vote or be revoked.
ineligibleTicket := ticketHashes[9]
// Add an additional vote transaction with a bad input. This ensures that a
// revocation is still created for a missed ticket due to a vote transaction
// failing validation.
vote, err := harness.CreateVote(tickets[numVotes])
if err != nil {
t.Fatalf("unable to create vote: %v", err)
}
_, err = harness.AddTransactionToTxSource(vote)
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
vote.MsgTx().TxIn[0].ValueIn = 0
// Get the best header bytes.
bestHeader, err := harness.chain.HeaderByHash(&harness.chain.bestState.Hash)
if err != nil {
t.Fatalf("unable to get tip block header %v: %v",
harness.chain.bestState.Hash, err)
}
bestHeaderBytes, err := bestHeader.Bytes()
if err != nil {
t.Fatalf("failed to serialize header for block %v: %v",
bestHeader.BlockHash(), err)
}
// Add revocations for tickets that were previously missed.
for _, ticketHash := range harness.chain.bestState.MissedTickets {
txDesc, err := harness.generator.createRevocationFromTicket(&ticketHash,
harness.chain.utxos, bestHeaderBytes,
harness.chain.isTreasuryAgendaActive)
if err != nil {
t.Fatalf("failed to create revocation for ticket %v: %v", ticketHash, err)
}
_, err = harness.AddTransactionToTxSource(txDesc.Tx)
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
}
// Add a revocation that tries to spend an ineligible ticket.
txDesc, err := harness.generator.createRevocationFromTicket(&ineligibleTicket,
harness.chain.utxos, bestHeaderBytes, harness.chain.isTreasuryAgendaActive)
if err != nil {
t.Fatalf("failed to create revocation for ticket %v: %v", ineligibleTicket,
err)
}
_, err = harness.AddTransactionToTxSource(txDesc.Tx)
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
// Add remaining regular transactions to the tx source.
for i := numVotes; i < numTxs; i++ {
_, err = harness.AddTransactionToTxSource(txs[i])
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
}
// Generate a new block template.
blockTemplate, err := harness.generator.NewBlockTemplate(address)
if err != nil {
t.Fatalf("unexpected err generating block template: %v", err)
}
// Validate the number of transactions in the generated block template.
gotTx := len(blockTemplate.Block.Transactions)
wantTx := numTxs - numVotes + 1 // + 1 for coinbase.
if gotTx != wantTx {
t.Fatalf("unexpected number of transactions in template -- got %v, want %v",
gotTx, wantTx)
}
// Determine the expected number of revocation transactions.
numAutoRevocations := len(missedThisBlock) + len(expiredThisBlock)
numTxSourceRevocations := len(missedPreviously)
numRevocations := numAutoRevocations + numTxSourceRevocations
// Validate the number of stake transactions in the generated block template.
gotStx := len(blockTemplate.Block.STransactions)
wantStx := numVotes + numRevocations + 1 // + 1 for stakebase.
if gotStx != wantStx {
t.Fatalf("unexpected number of stake transactions in template -- got %v, "+
"want %v", gotStx, wantStx)
}
// Validate that the block is sane. These checks are context free.
block := dcrutil.NewBlock(blockTemplate.Block)
err = blockchain.CheckBlockSanity(block, harness.generator.cfg.TimeSource,
harness.chainParams)
if err != nil {
t.Fatalf("unexpected error when checking block sanity: %v", err)
}
}
// TestNewBlockTemplateAutoRevocationsVotesOnly tests that auto revocations are
// properly added to blocks that only contain vote transactions.
func TestNewBlockTemplateAutoRevocationsVotesOnly(t *testing.T) {
t.Parallel()
// Create a new mining harness instance.
harness, spendableOuts, err := newMiningHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("error creating mining harness: %v", err)
}
// Activate the automatic ticket revocations agenda.
harness.chain.isAutoRevocationsAgendaActive = true
// Create a test address for use in template generation.
address, err := stdaddr.DecodeAddress("Dsi8CRt85xYyempXs7ZPL1rBxvDdAGZmgsg",
harness.chainParams)
if err != nil {
t.Fatalf("error decoding address: %v", err)
}
// Define a munger to apply transaction fees.
applyTxFee := func(fee int64) func(*wire.MsgTx) {
return func(tx *wire.MsgTx) {
tx.TxOut[0].Value -= fee
}
}
// Create additional transactions from the first spendable output provided by
// the harness.
const numTxs = 6
txs := make([]*dcrutil.Tx, numTxs)
baseTx, err := harness.CreateSignedTx(spendableOuts, uint32(numTxs))
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
harness.AddFakeUTXO(baseTx, harness.chain.bestState.Height, 1,
harness.chain.isTreasuryAgendaActive)
for i := 0; i < numTxs; i++ {
tx, err := harness.CreateSignedTx([]spendableOutput{
txOutToSpendableOut(baseTx, uint32(i), wire.TxTreeRegular)}, 1,
applyTxFee(5000))
if err != nil {
t.Fatalf("unable to create transaction: %v", err)
}
txs[i] = tx
}
// Create ticket purchase transactions spending the outputs of the prior
// regular transactions.
const numTickets = 5
tickets := make([]*dcrutil.Tx, numTickets)
ticketHashes := make([]chainhash.Hash, numTickets)
for i := 0; i < numTickets; i++ {
ticket, err := harness.CreateTicketPurchase(txs[i], 40000)
if err != nil {
t.Fatalf("unable to create ticket purchase transaction: %v", err)
}
tickets[i] = ticket
ticketHashes[i] = ticket.MsgTx().TxHash()
}
// Add the ticket outputs as utxos to fake their existence. Use one after
// the stake enabled height for the height of the fake utxos to ensure they
// are mature for the votes cast at stake validation height below.
harness.chain.bestState.Height = harness.chainParams.StakeEnabledHeight + 1
for i, ticket := range tickets {
harness.AddFakeUTXO(ticket, harness.chain.bestState.Height, uint32(i+1),
harness.chain.isTreasuryAgendaActive)
}
// Create votes on a block at stake validation height using the previously
// created tickets.
//
// Create votes for 3 out of 5 winning tickets. The 2 remaining winning
// tickets will be missed and will need to be revoked.
winningTickets := ticketHashes[:5]
voteTickets := winningTickets[:3]
missedThisBlock := winningTickets[3:]
harness.chain.bestState.Height = harness.chainParams.StakeValidationHeight
harness.chain.bestState.NextWinningTickets = winningTickets
numVotes := len(voteTickets)
votes := make([]*dcrutil.Tx, numVotes)
for i, ticket := range tickets[:numVotes] {
vote, err := harness.CreateVote(ticket)
if err != nil {
t.Fatalf("unable to create vote: %v", err)
}
votes[i] = vote
}
// Add vote transactions to the tx source.
for _, vote := range votes {
_, err = harness.AddTransactionToTxSource(vote)
if err != nil {
t.Fatalf("unable to add transaction to the tx source: %v", err)
}
}
// Generate a new block template.
blockTemplate, err := harness.generator.NewBlockTemplate(address)
if err != nil {
t.Fatalf("unexpected err generating block template: %v", err)
}
// Determine the expected number of revocation transactions.
numAutoRevocations := len(missedThisBlock)
// Validate the number of stake transactions in the generated block template.
gotStx := len(blockTemplate.Block.STransactions)
wantStx := numVotes + numAutoRevocations + 1 // + 1 for stakebase.
if gotStx != wantStx {
t.Fatalf("unexpected number of stake transactions in template -- got %v, "+
"want %v", gotStx, wantStx)
}
// Validate that the block is sane. These checks are context free.
block := dcrutil.NewBlock(blockTemplate.Block)
err = blockchain.CheckBlockSanity(block, harness.generator.cfg.TimeSource,
harness.chainParams)
if err != nil {
t.Fatalf("unexpected error when checking block sanity: %v", err)
}
}
// TestSortParentsByVotes ensures the function that sorts parent blocks based on
// the number of votes available for them and the current tip block works as
// intended, including reorg prevention for an equal number of votes.
func TestSortParentsByVotes(t *testing.T) {
params := chaincfg.MainNetParams()
tests := []struct {
name string // test description
numBlocks uint8 // number of sibling blocks to generate
votes []uint8 // mock votes to generate per block
curTip uint8 // index of the current tip block
want []uint8 // indexes of expected sorted blocks
}{{
name: "0 blocks, 0 votes (sanity check for impossible condition)",
numBlocks: 0,
votes: nil,
curTip: 0,
want: nil,
}, {
name: "1 block, 5 votes",
numBlocks: 1,
votes: []uint8{5},
curTip: 0,
want: []uint8{0},
}, {
name: "1 block, 4 votes",
numBlocks: 1,
votes: []uint8{4},
curTip: 0,
want: []uint8{0},
}, {
name: "1 block, 3 votes",
numBlocks: 1,
votes: []uint8{3},
curTip: 0,
want: []uint8{0},
}, {
name: "1 block, 2 votes (no useful blocks)",
numBlocks: 1,
votes: []uint8{2},
curTip: 0,
want: nil,
}, {
name: "2 blocks, votes{0:5, 1:5}, tip block 0 (no reorg)",
numBlocks: 2,
votes: []uint8{5, 5},
curTip: 0,
want: []uint8{0, 1},
}, {
name: "2 blocks, votes{0:5, 1:5}, tip block 1 (no reorg)",
numBlocks: 2,
votes: []uint8{5, 5},
curTip: 1,
want: []uint8{1, 0},
}, {
name: "2 blocks, votes{0:4, 1:5}, tip block 0",
numBlocks: 2,
votes: []uint8{4, 5},
curTip: 0,
want: []uint8{1, 0},
}, {
name: "2 blocks, votes{0:4, 1:5}, tip block 1",
numBlocks: 2,
votes: []uint8{4, 5},
curTip: 1,
want: []uint8{1, 0},
}, {
name: "3 blocks, votes{0:4, 1:5, 2:2}, tip block 0 (2 not useful)",
numBlocks: 3,
votes: []uint8{4, 5, 2},
curTip: 0,
want: []uint8{1, 0},
}, {
name: "3 blocks, votes{0:3, 1:5, 2:4}, tip block 0",
numBlocks: 3,
votes: []uint8{3, 5, 4},
curTip: 0,
want: []uint8{1, 2, 0},
}, {
name: "3 blocks, votes{0:3, 1:5, 2:4}, tip block 2",
numBlocks: 3,
votes: []uint8{3, 5, 4},
curTip: 2,
want: []uint8{1, 2, 0},
}, {
name: "3 blocks, votes{0:3, 1:3, 2:4}, tip block 1",
numBlocks: 3,
votes: []uint8{3, 3, 4},
curTip: 1,
want: []uint8{2, 0, 1},
}}
// nextHash is a convenience function that generates a unique hash each time
// it is called.
var genHashIndex uint32
nextHash := func() chainhash.Hash {
var buf [4]byte
binary.LittleEndian.PutUint32(buf[:], genHashIndex)
genHashIndex++
return chainhash.HashH(buf[:])
}
for _, test := range tests {
// Generate mock blocks and votes based on test data.
blockHashes := make([]chainhash.Hash, test.numBlocks)
for i := uint8(0); i < test.numBlocks; i++ {
blockHashes[i] = nextHash()
}
votes := make(map[chainhash.Hash][]VoteDesc, len(test.votes))
for i, numVotes := range test.votes {
blockVotedOn := blockHashes[i]
voteDescs := make([]VoteDesc, 0, numVotes)
for i := uint8(0); i < numVotes; i++ {
voteDescs = append(voteDescs, VoteDesc{
VoteHash: nextHash(),
TicketHash: nextHash(),
ApprovesParent: true,
})
}
votes[blockVotedOn] = voteDescs
}
var curTip chainhash.Hash
if len(blockHashes) == 0 {
curTip = nextHash()
} else {
curTip = blockHashes[test.curTip]
}
var want []chainhash.Hash
if len(test.want) > 0 {
want = make([]chainhash.Hash, 0, len(test.want))
for _, blockIndex := range test.want {
want = append(want, blockHashes[blockIndex])
}
}
// Create a fake transaction source instance populated with the votes.
txSource := &fakeTxSource{
votes: votes,
}
// Ensure the result matches the expected values.
got := SortParentsByVotes(txSource, curTip, blockHashes, params)
if !reflect.DeepEqual(got, want) {
t.Errorf("%q: unexpected result -- got %v, want %v", test.name, got,
want)
continue
}
}
}