dcrd/internal/mining/mining_test.go
Ryan Staudt 20300ed0bc mining: Add auto revocation block template tests.
This adds tests to ensure that creating new block templates works as
expected when the automatic ticket revocations agenda is enabled.
2021-09-20 18:02:19 -05:00

521 lines
18 KiB
Go

// Copyright (c) 2020-2021 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 (
"errors"
"testing"
"github.com/decred/dcrd/blockchain/v4"
"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)
}
}