This adds tests to ensure that creating new block templates works as expected when the automatic ticket revocations agenda is enabled.
521 lines
18 KiB
Go
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)
|
|
}
|
|
}
|