This removes the flags param from the ProcessBlockHeader function in blockchain since it is now passed in as BFNone in all instances. The appropriate flags will now be determined internally within blockchain.
1498 lines
51 KiB
Go
1498 lines
51 KiB
Go
// Copyright (c) 2013-2016 The btcsuite 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.
|
|
|
|
package blockchain
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
mrand "math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/decred/dcrd/blockchain/stake/v4"
|
|
"github.com/decred/dcrd/blockchain/v4/chaingen"
|
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
|
"github.com/decred/dcrd/chaincfg/v3"
|
|
"github.com/decred/dcrd/database/v3"
|
|
_ "github.com/decred/dcrd/database/v3/ffldb"
|
|
"github.com/decred/dcrd/dcrutil/v4"
|
|
"github.com/decred/dcrd/lru"
|
|
"github.com/decred/dcrd/txscript/v4"
|
|
"github.com/decred/dcrd/txscript/v4/sign"
|
|
"github.com/decred/dcrd/wire"
|
|
"github.com/syndtr/goleveldb/leveldb"
|
|
"github.com/syndtr/goleveldb/leveldb/filter"
|
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
|
)
|
|
|
|
const (
|
|
// testDbType is the database backend type to use for the tests.
|
|
testDbType = "ffldb"
|
|
|
|
// blockDataNet is the expected network in the test block data.
|
|
blockDataNet = wire.MainNet
|
|
)
|
|
|
|
// isSupportedDbType returns whether or not the passed database type is
|
|
// currently supported.
|
|
func isSupportedDbType(dbType string) bool {
|
|
supportedDrivers := database.SupportedDrivers()
|
|
for _, driver := range supportedDrivers {
|
|
if dbType == driver {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// createTestDatabase creates a test database with the provided database name
|
|
// and database type for the given network.
|
|
func createTestDatabase(dbName string, dbType string, net wire.CurrencyNet) (database.DB, func(), error) {
|
|
// Handle memory database specially since it doesn't need the disk specific
|
|
// handling.
|
|
var db database.DB
|
|
var teardown func()
|
|
if dbType == "memdb" {
|
|
ndb, err := database.Create(dbType)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error creating db: %w", err)
|
|
}
|
|
db = ndb
|
|
|
|
// Setup a teardown function for cleaning up. This function is returned to
|
|
// the caller to be invoked when it is done testing.
|
|
teardown = func() {
|
|
db.Close()
|
|
}
|
|
} else {
|
|
// Create the directory for the test database.
|
|
dbPath, err := os.MkdirTemp("", dbName)
|
|
if err != nil {
|
|
err := fmt.Errorf("unable to create test db path: %w",
|
|
err)
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Create the test database.
|
|
ndb, err := database.Create(dbType, dbPath, net)
|
|
if err != nil {
|
|
os.RemoveAll(dbPath)
|
|
return nil, nil, fmt.Errorf("error creating db: %w", err)
|
|
}
|
|
db = ndb
|
|
|
|
// Setup a teardown function for cleaning up. This function is returned to
|
|
// the caller to be invoked when it is done testing.
|
|
teardown = func() {
|
|
db.Close()
|
|
os.RemoveAll(dbPath)
|
|
}
|
|
}
|
|
|
|
return db, teardown, nil
|
|
}
|
|
|
|
// createTestUtxoDatabase creates a test UTXO database with the provided
|
|
// database name.
|
|
func createTestUtxoDatabase(dbName string) (*leveldb.DB, func(), error) {
|
|
// Construct the database filepath and remove all from that path.
|
|
dbPath := filepath.Join(os.TempDir(), dbName)
|
|
_ = os.RemoveAll(dbPath)
|
|
|
|
// Open the database (will create it if needed).
|
|
opts := opt.Options{
|
|
Strict: opt.DefaultStrict,
|
|
Compression: opt.NoCompression,
|
|
Filter: filter.NewBloomFilter(10),
|
|
}
|
|
db, err := leveldb.OpenFile(dbPath, &opts)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Setup a teardown function for cleaning up. This function is returned to
|
|
// the caller to be invoked when it is done testing.
|
|
teardown := func() {
|
|
_ = db.Close()
|
|
_ = os.RemoveAll(dbPath)
|
|
}
|
|
|
|
return db, teardown, nil
|
|
}
|
|
|
|
// chainSetup is used to create a new db and chain instance with the genesis
|
|
// block already inserted. In addition to the new chain instance, it returns
|
|
// a teardown function the caller should invoke when done testing to clean up.
|
|
func chainSetup(dbName string, params *chaincfg.Params) (*BlockChain, func(), error) {
|
|
if !isSupportedDbType(testDbType) {
|
|
return nil, nil, fmt.Errorf("unsupported db type %v", testDbType)
|
|
}
|
|
|
|
// Create a test block database.
|
|
db, teardownDb, err := createTestDatabase(dbName, testDbType, blockDataNet)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Create a test UTXO database.
|
|
utxoDb, teardownUtxoDb, err := createTestUtxoDatabase(dbName + "_utxo")
|
|
if err != nil {
|
|
teardownDb()
|
|
return nil, nil, err
|
|
}
|
|
teardown := func() {
|
|
teardownUtxoDb()
|
|
teardownDb()
|
|
}
|
|
|
|
// Copy the chain params to ensure any modifications the tests do to
|
|
// the chain parameters do not affect the global instance.
|
|
paramsCopy := *params
|
|
|
|
// Create a SigCache instance.
|
|
sigCache, err := txscript.NewSigCache(1000)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Create the main chain instance.
|
|
utxoBackend := NewLevelDbUtxoBackend(utxoDb)
|
|
chain, err := New(context.Background(),
|
|
&Config{
|
|
DB: db,
|
|
UtxoBackend: utxoBackend,
|
|
ChainParams: ¶msCopy,
|
|
TimeSource: NewMedianTime(),
|
|
SigCache: sigCache,
|
|
UtxoCache: NewUtxoCache(&UtxoCacheConfig{
|
|
Backend: utxoBackend,
|
|
FlushBlockDB: func() error {
|
|
// Don't flush to disk since it is slow and this is used in a lot of
|
|
// tests.
|
|
return nil
|
|
},
|
|
MaxSize: 100 * 1024 * 1024, // 100 MiB
|
|
}),
|
|
})
|
|
|
|
if err != nil {
|
|
teardown()
|
|
err := fmt.Errorf("failed to create chain instance: %w", err)
|
|
return nil, nil, err
|
|
}
|
|
|
|
return chain, teardown, nil
|
|
}
|
|
|
|
// newFakeChain returns a chain that is usable for synthetic tests. It is
|
|
// important to note that this chain has no database associated with it, so
|
|
// it is not usable with all functions and the tests must take care when making
|
|
// use of it.
|
|
func newFakeChain(params *chaincfg.Params) *BlockChain {
|
|
// Create a genesis block node and block index populated with it for use
|
|
// when creating the fake chain below.
|
|
node := newBlockNode(¶ms.GenesisBlock.Header, nil)
|
|
node.status = statusDataStored | statusValidated
|
|
node.isFullyLinked = true
|
|
index := newBlockIndex(nil)
|
|
index.bestHeader = node
|
|
index.AddNode(node)
|
|
|
|
// Generate a deployment ID to version map from the provided params.
|
|
deploymentVers, err := extractDeploymentIDVersions(params)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return &BlockChain{
|
|
deploymentVers: deploymentVers,
|
|
chainParams: params,
|
|
deploymentCaches: newThresholdCaches(params),
|
|
index: index,
|
|
bestChain: newChainView(node),
|
|
recentBlocks: lru.NewKVCache(recentBlockCacheSize),
|
|
isVoterMajorityVersionCache: make(map[[stakeMajorityCacheKeySize]byte]bool),
|
|
isStakeMajorityVersionCache: make(map[[stakeMajorityCacheKeySize]byte]bool),
|
|
calcPriorStakeVersionCache: make(map[[chainhash.HashSize]byte]uint32),
|
|
calcVoterVersionIntervalCache: make(map[[chainhash.HashSize]byte]uint32),
|
|
calcStakeVersionCache: make(map[[chainhash.HashSize]byte]uint32),
|
|
}
|
|
}
|
|
|
|
// testNoncePrng provides a deterministic prng for the nonce in generated fake
|
|
// nodes. This ensures that the nodes have unique hashes.
|
|
var testNoncePrng = mrand.New(mrand.NewSource(0))
|
|
|
|
// newFakeNode creates a block node connected to the passed parent with the
|
|
// provided fields populated and fake values for the other fields.
|
|
func newFakeNode(parent *blockNode, blockVersion int32, stakeVersion uint32, bits uint32, timestamp time.Time) *blockNode {
|
|
// Make up a header and create a block node from it.
|
|
var prevHash chainhash.Hash
|
|
var height uint32
|
|
if parent != nil {
|
|
prevHash = parent.hash
|
|
height = uint32(parent.height + 1)
|
|
}
|
|
header := &wire.BlockHeader{
|
|
Version: blockVersion,
|
|
PrevBlock: prevHash,
|
|
VoteBits: 0x01,
|
|
Bits: bits,
|
|
Height: height,
|
|
Timestamp: timestamp,
|
|
Nonce: testNoncePrng.Uint32(),
|
|
StakeVersion: stakeVersion,
|
|
}
|
|
node := newBlockNode(header, parent)
|
|
node.status = statusDataStored | statusValidated
|
|
node.isFullyLinked = parent == nil || parent.isFullyLinked
|
|
return node
|
|
}
|
|
|
|
type treasuryVoteTuple struct {
|
|
tspendHash chainhash.Hash
|
|
vote byte
|
|
}
|
|
|
|
// newFakeCreateVoteTx creates a fake vote transaction and appends treasury
|
|
// votes if provided.
|
|
func newFakeCreateVoteTx(tspendVotes []treasuryVoteTuple) *wire.MsgTx {
|
|
var (
|
|
voteSubsidy = 5000000000
|
|
ticketPrice = 100000000
|
|
opTrueRedeemScript = []byte{txscript.OP_DATA_1, txscript.OP_TRUE}
|
|
stakeGenScript = [26]byte{txscript.OP_SSGEN}
|
|
blockScript = [38]byte{txscript.OP_RETURN, txscript.OP_DATA_36}
|
|
voteScript = [4]byte{txscript.OP_RETURN, txscript.OP_DATA_2}
|
|
)
|
|
// Fake out stakeGenScript.
|
|
tagOffset := 1 // Prefixed with OP_SSGEN
|
|
stakeGenScript[tagOffset+0] = txscript.OP_DUP
|
|
stakeGenScript[tagOffset+1] = txscript.OP_HASH160
|
|
stakeGenScript[tagOffset+2] = txscript.OP_DATA_20
|
|
stakeGenScript[tagOffset+23] = txscript.OP_EQUALVERIFY
|
|
stakeGenScript[tagOffset+24] = txscript.OP_CHECKSIG
|
|
|
|
// Generate and return the transaction with the proof-of-stake subsidy
|
|
// coinbase and spending from the provided ticket along with the
|
|
// previously described outputs.
|
|
ticketHash := &chainhash.Hash{}
|
|
tx := wire.NewMsgTx()
|
|
tx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{},
|
|
wire.MaxPrevOutIndex, wire.TxTreeRegular),
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: int64(voteSubsidy),
|
|
BlockHeight: wire.NullBlockHeight,
|
|
BlockIndex: wire.NullBlockIndex,
|
|
})
|
|
tx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: *wire.NewOutPoint(ticketHash, 0,
|
|
wire.TxTreeStake),
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: int64(ticketPrice),
|
|
SignatureScript: opTrueRedeemScript,
|
|
})
|
|
tx.AddTxOut(wire.NewTxOut(0, blockScript[:]))
|
|
tx.AddTxOut(wire.NewTxOut(0, voteScript[:]))
|
|
tx.AddTxOut(wire.NewTxOut(int64(voteSubsidy+ticketPrice),
|
|
stakeGenScript[:]))
|
|
|
|
// Append tspend votes if set.
|
|
if tspendVotes != nil {
|
|
tx.Version = wire.TxVersionTreasury
|
|
tspendScript := make([]byte, 0, 256)
|
|
tspendScript = append(tspendScript, txscript.OP_RETURN,
|
|
byte(len(tspendVotes)*33+2), 'T', 'V')
|
|
for _, v := range tspendVotes {
|
|
tspendScript = append(tspendScript, v.tspendHash[:]...)
|
|
tspendScript = append(tspendScript, v.vote)
|
|
}
|
|
tx.AddTxOut(wire.NewTxOut(0, tspendScript))
|
|
}
|
|
return tx
|
|
}
|
|
|
|
// newFakeCreateTSpend creates a fake tspend transaction.
|
|
func newFakeCreateTSpend(privKey []byte, payouts []dcrutil.Amount, fee dcrutil.Amount, expiry uint32) *wire.MsgTx {
|
|
// Calculate total payout.
|
|
totalPayout := int64(0)
|
|
for _, amount := range payouts {
|
|
totalPayout += int64(amount)
|
|
}
|
|
valueIn := int64(fee) + totalPayout
|
|
|
|
// OP_RETURN <8 byte LE ValueIn><24 byte random>
|
|
// The TSpend TxIn ValueIn is encoded in the first 8 bytes to ensure
|
|
// that it becomes signed. This is consensus enforced.
|
|
var payload [32]byte
|
|
binary.LittleEndian.PutUint64(payload[0:8], uint64(valueIn))
|
|
_, err := mrand.Read(payload[8:])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
builder := txscript.NewScriptBuilder()
|
|
builder.AddOp(txscript.OP_RETURN)
|
|
builder.AddData(payload[:])
|
|
opretScript, err := builder.Script()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
msgTx := wire.NewMsgTx()
|
|
msgTx.Version = wire.TxVersionTreasury
|
|
msgTx.Expiry = expiry
|
|
msgTx.AddTxOut(wire.NewTxOut(0, opretScript))
|
|
|
|
// OP_TGEN
|
|
for _, amount := range payouts {
|
|
// Generate valid script. This is a hex encoded blob from
|
|
// generator.go.
|
|
p2shOpTrueScript, err := hex.DecodeString("a914f5a8302ee8695bf836258b8f2b57b38a0be14e4787")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
script := make([]byte, len(p2shOpTrueScript)+1)
|
|
script[0] = txscript.OP_TGEN
|
|
copy(script[1:], p2shOpTrueScript[:])
|
|
msgTx.AddTxOut(wire.NewTxOut(int64(amount), script))
|
|
}
|
|
|
|
// Treasury spend transactions have no inputs since the funds are
|
|
// sourced from a special account, so previous outpoint is zero hash
|
|
// and max index.
|
|
msgTx.AddTxIn(&wire.TxIn{
|
|
PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{},
|
|
wire.MaxPrevOutIndex, wire.TxTreeRegular),
|
|
Sequence: wire.MaxTxInSequenceNum,
|
|
ValueIn: valueIn,
|
|
BlockHeight: wire.NullBlockHeight,
|
|
BlockIndex: wire.NullBlockIndex,
|
|
SignatureScript: nil,
|
|
})
|
|
|
|
// Calculate TSpend signature without SigHashType.
|
|
sigscript, err := sign.TSpendSignatureScript(msgTx, privKey)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
msgTx.TxIn[0].SignatureScript = sigscript
|
|
|
|
return msgTx
|
|
}
|
|
|
|
// chainedFakeNodes returns the specified number of nodes constructed such that
|
|
// each subsequent node points to the previous one to create a chain. The first
|
|
// node will point to the passed parent which can be nil if desired.
|
|
func chainedFakeNodes(parent *blockNode, numNodes int) []*blockNode {
|
|
nodes := make([]*blockNode, numNodes)
|
|
tip := parent
|
|
blockTime := time.Now()
|
|
if tip != nil {
|
|
blockTime = time.Unix(tip.timestamp, 0)
|
|
}
|
|
for i := 0; i < numNodes; i++ {
|
|
blockTime = blockTime.Add(time.Second)
|
|
node := newFakeNode(tip, 1, 1, 0, blockTime)
|
|
tip = node
|
|
|
|
nodes[i] = node
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// chainedFakeSkipListNodes returns the specified number of nodes populated with
|
|
// only the fields specifically needed to test the skip list functionality and
|
|
// constructed such that each subsequent node points to the previous one to
|
|
// create a chain. The first node will point to the passed parent which can be
|
|
// nil if desired.
|
|
//
|
|
// This is used over the chainedFakeNodes function for skip list testing because
|
|
// the skip list tests involve large numbers of nodes which take much longer to
|
|
// create with all of the other fields populated by said function.
|
|
func chainedFakeSkipListNodes(parent *blockNode, numNodes int) []*blockNode {
|
|
nodes := make([]*blockNode, numNodes)
|
|
for i := 0; i < numNodes; i++ {
|
|
node := &blockNode{parent: parent, height: int64(i)}
|
|
if parent != nil {
|
|
node.skipToAncestor = nodes[calcSkipListHeight(int64(i))]
|
|
}
|
|
parent = node
|
|
|
|
nodes[i] = node
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// branchTip is a convenience function to grab the tip of a chain of block nodes
|
|
// created via chainedFakeNodes.
|
|
func branchTip(nodes []*blockNode) *blockNode {
|
|
return nodes[len(nodes)-1]
|
|
}
|
|
|
|
// appendFakeVotes appends the passed number of votes to the node with the
|
|
// provided version and vote bits.
|
|
func appendFakeVotes(node *blockNode, numVotes uint16, voteVersion uint32, voteBits uint16) {
|
|
for i := uint16(0); i < numVotes; i++ {
|
|
node.votes = append(node.votes, stake.VoteVersionTuple{
|
|
Version: voteVersion,
|
|
Bits: voteBits,
|
|
})
|
|
}
|
|
}
|
|
|
|
// findDeployment finds the provided vote ID within the deployments of the
|
|
// provided parameters and either returns the deployment version it was found in
|
|
// along with a pointer to the deployment or an error when not found.
|
|
func findDeployment(params *chaincfg.Params, voteID string) (uint32, *chaincfg.ConsensusDeployment, error) {
|
|
// Find the correct deployment for the passed vote ID.
|
|
for version, deployments := range params.Deployments {
|
|
for i, deployment := range deployments {
|
|
if deployment.Vote.Id == voteID {
|
|
return version, &deployments[i], nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0, nil, fmt.Errorf("unable to find deployment for id %q", voteID)
|
|
}
|
|
|
|
// findDeploymentChoice finds the provided choice ID within the given
|
|
// deployment params and either returns a pointer to the found choice or an
|
|
// error when not found.
|
|
func findDeploymentChoice(deployment *chaincfg.ConsensusDeployment, choiceID string) (*chaincfg.Choice, error) {
|
|
// Find the correct choice for the passed choice ID.
|
|
for i, choice := range deployment.Vote.Choices {
|
|
if choice.Id == choiceID {
|
|
return &deployment.Vote.Choices[i], nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("unable to find vote choice for id %q", choiceID)
|
|
}
|
|
|
|
// findDeploymentAllYesChoices returns the deployment version and the OR'ed sum
|
|
// of the 'yes' choices for all the passed agendas.
|
|
//
|
|
// Note that all agendas must be in the same deployment version or this
|
|
// function errors.
|
|
func findDeploymentAllYesChoices(params *chaincfg.Params, voteIDs []string) (uint32, uint16, error) {
|
|
var yesBits uint16
|
|
var deploymentVer uint32
|
|
var foundIDs int
|
|
for version, deployments := range params.Deployments {
|
|
for _, deployment := range deployments {
|
|
for _, wantID := range voteIDs {
|
|
if deployment.Vote.Id == wantID {
|
|
yesChoice, err := findDeploymentChoice(&deployment, "yes")
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
deploymentVer = version
|
|
foundIDs++
|
|
yesBits |= yesChoice.Bits
|
|
}
|
|
}
|
|
}
|
|
|
|
if foundIDs == 0 {
|
|
continue
|
|
}
|
|
|
|
// Agendas can only be voted simultaneously if they belong to
|
|
// the same deployment version, so return an error if we found
|
|
// some, but not all vote IDs in a specific version.
|
|
if foundIDs != len(voteIDs) {
|
|
return 0, 0, fmt.Errorf("not all voteIDs were found " +
|
|
"in the same deployment version")
|
|
}
|
|
|
|
return deploymentVer, yesBits, nil
|
|
}
|
|
|
|
return 0, 0, fmt.Errorf("unable to find deployment for any of the " +
|
|
"provided vote IDs")
|
|
}
|
|
|
|
// mergeAgendas moves all the specified agendas into the same deployment,
|
|
// suitable for being voted at the same time.
|
|
//
|
|
// The agendas will be moved to the deployment that contains the first agenda
|
|
// in the voteIDs slice.
|
|
//
|
|
// Returns the resulting deployment version.
|
|
func mergeAgendas(params *chaincfg.Params, voteIDs []string) (uint32, error) {
|
|
if len(voteIDs) < 2 {
|
|
return 0, fmt.Errorf("at least 2 agenda IDs must be specified")
|
|
}
|
|
|
|
// The first agenda defines the destination deployment, where the
|
|
// others will be moved to.
|
|
targetDeployVer, _, err := findDeployment(params, voteIDs[0])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
voteIDs = voteIDs[1:]
|
|
targetDeploy := params.Deployments[targetDeployVer]
|
|
|
|
nextvote:
|
|
for _, wantID := range voteIDs {
|
|
for version, deployments := range params.Deployments {
|
|
for i := 0; i < len(deployments); {
|
|
deployment := deployments[i]
|
|
|
|
if deployment.Vote.Id != wantID {
|
|
i++
|
|
continue
|
|
}
|
|
|
|
// Found where the agenda is. Move to target.
|
|
targetDeploy = append(targetDeploy, deployment)
|
|
params.Deployments[targetDeployVer] = targetDeploy
|
|
deployments[i] = deployments[len(deployments)-1]
|
|
deployments = deployments[:len(deployments)-1]
|
|
params.Deployments[version] = deployments
|
|
continue nextvote
|
|
}
|
|
}
|
|
|
|
return 0, fmt.Errorf("unable to find vote id %s", wantID)
|
|
}
|
|
|
|
return targetDeployVer, nil
|
|
}
|
|
|
|
// removeDeployment modifies the passed parameters to remove all deployments for
|
|
// the provided vote ID. An error is returned when not found.
|
|
func removeDeployment(params *chaincfg.Params, voteID string) error {
|
|
// Remove the deployment(s) for the passed vote ID.
|
|
var found bool
|
|
for version, deployments := range params.Deployments {
|
|
for i, deployment := range deployments {
|
|
if deployment.Vote.Id == voteID {
|
|
copy(deployments[i:], deployments[i+1:])
|
|
deployments[len(deployments)-1] = chaincfg.ConsensusDeployment{}
|
|
deployments = deployments[:len(deployments)-1]
|
|
params.Deployments[version] = deployments
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
if found {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("unable to find deployment for id %q", voteID)
|
|
}
|
|
|
|
// removeDeploymentTimeConstraints modifies the passed deployment to remove the
|
|
// voting time constraints by making it always available to vote and to never
|
|
// expire.
|
|
//
|
|
// NOTE: This will mutate the passed deployment, so ensure this function is
|
|
// only called with parameters that are not globally available.
|
|
func removeDeploymentTimeConstraints(deployment *chaincfg.ConsensusDeployment) {
|
|
deployment.StartTime = 0 // Always available for vote.
|
|
deployment.ExpireTime = math.MaxUint64 // Never expires.
|
|
}
|
|
|
|
// chaingenHarness provides a test harness which encapsulates a test instance, a
|
|
// chaingen generator instance, and a block chain instance to provide all of the
|
|
// functionality of the aforementioned types as well as several convenience
|
|
// functions such as block acceptance and rejection, expected tip checking, and
|
|
// threshold state checking.
|
|
//
|
|
// The chaingen generator is embedded in the struct so callers can directly
|
|
// access its method the same as if they were directly working with the
|
|
// underlying generator.
|
|
//
|
|
// Since chaingen involves creating fully valid and solved blocks, which is
|
|
// relatively expensive, only tests which actually require that functionality
|
|
// should make use of this harness. In many cases, a much faster synthetic
|
|
// chain instance created by newFakeChain will suffice.
|
|
type chaingenHarness struct {
|
|
*chaingen.Generator
|
|
|
|
t *testing.T
|
|
chain *BlockChain
|
|
deploymentVersions map[string]uint32
|
|
}
|
|
|
|
// newChaingenHarnessWithGen creates and returns a new instance of a chaingen
|
|
// harness that encapsulates the provided test instance and existing chaingen
|
|
// generator along with a teardown function the caller should invoke when done
|
|
// testing to clean up.
|
|
//
|
|
// This differs from newChaingenHarness in that it allows the caller to provide
|
|
// a chaingen generator to use instead of creating a new one.
|
|
//
|
|
// See the documentation for the chaingenHarness type for more details.
|
|
func newChaingenHarnessWithGen(t *testing.T, dbName string, g *chaingen.Generator) (*chaingenHarness, func()) {
|
|
t.Helper()
|
|
|
|
// Create a new database and chain instance to run tests against.
|
|
chain, teardownFunc, err := chainSetup(dbName, g.Params())
|
|
if err != nil {
|
|
t.Fatalf("Failed to setup chain instance: %v", err)
|
|
}
|
|
|
|
harness := chaingenHarness{
|
|
Generator: g,
|
|
t: t,
|
|
chain: chain,
|
|
deploymentVersions: make(map[string]uint32),
|
|
}
|
|
return &harness, teardownFunc
|
|
}
|
|
|
|
// newChaingenHarness creates and returns a new instance of a chaingen harness
|
|
// that encapsulates the provided test instance along with a teardown function
|
|
// the caller should invoke when done testing to clean up.
|
|
//
|
|
// This differs from newChaingenHarnessWithGen in that it creates a new chaingen
|
|
// generator to use instead of allowing the caller to provide one.
|
|
//
|
|
// See the documentation for the chaingenHarness type for more details.
|
|
func newChaingenHarness(t *testing.T, params *chaincfg.Params, dbName string) (*chaingenHarness, func()) {
|
|
t.Helper()
|
|
|
|
// Create a test generator instance initialized with the genesis block as
|
|
// the tip.
|
|
g, err := chaingen.MakeGenerator(params)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create generator: %v", err)
|
|
}
|
|
|
|
return newChaingenHarnessWithGen(t, dbName, &g)
|
|
}
|
|
|
|
// AcceptHeader processes the block header associated with the given name in the
|
|
// harness generator and expects it to be accepted, but not necessarily to the
|
|
// main chain. It also ensures the underlying block index is consistent with
|
|
// the result.
|
|
func (g *chaingenHarness) AcceptHeader(blockName string) {
|
|
g.t.Helper()
|
|
|
|
header := &g.BlockByName(blockName).Header
|
|
blockHash := header.BlockHash()
|
|
blockHeight := header.Height
|
|
g.t.Logf("Testing accept block header %q (hash %s, height %d)", blockName,
|
|
blockHash, blockHeight)
|
|
|
|
// Determine if the header is already known before attempting to process it.
|
|
alreadyHaveHeader := g.chain.index.LookupNode(&blockHash) != nil
|
|
|
|
err := g.chain.ProcessBlockHeader(header)
|
|
if err != nil {
|
|
g.t.Fatalf("block header %q (hash %s, height %d) should have been "+
|
|
"accepted: %v", blockName, blockHash, blockHeight, err)
|
|
}
|
|
|
|
// Ensure the accepted header now exists in the block index.
|
|
node := g.chain.index.LookupNode(&blockHash)
|
|
if node == nil {
|
|
g.t.Fatalf("accepted block header %q (hash %s, height %d) should have "+
|
|
"been added to the block index", blockName, blockHash, blockHeight)
|
|
}
|
|
|
|
// Ensure the accepted header is not marked as known valid when it was not
|
|
// previously known since that implies the block data is not yet available
|
|
// and therefore it can't possibly be known to be valid.
|
|
//
|
|
// Also, ensure the accepted header is not marked as known invalid, as
|
|
// having known invalid ancestors, or as known to have failed validation.
|
|
status := g.chain.index.NodeStatus(node)
|
|
if !alreadyHaveHeader && status.HasValidated() {
|
|
g.t.Fatalf("accepted block header %q (hash %s, height %d) was not "+
|
|
"already known, but is marked as known valid", blockName, blockHash,
|
|
blockHeight)
|
|
}
|
|
if status.KnownInvalid() {
|
|
g.t.Fatalf("accepted block header %q (hash %s, height %d) is marked "+
|
|
"as known invalid", blockName, blockHash, blockHeight)
|
|
}
|
|
if status.KnownInvalidAncestor() {
|
|
g.t.Fatalf("accepted block header %q (hash %s, height %d) is marked "+
|
|
"as having a known invalid ancestor", blockName, blockHash,
|
|
blockHeight)
|
|
}
|
|
if status.KnownValidateFailed() {
|
|
g.t.Fatalf("accepted block header %q (hash %s, height %d) is marked "+
|
|
"as having known to fail validation", blockName, blockHash,
|
|
blockHeight)
|
|
}
|
|
}
|
|
|
|
// AcceptBlockData processes the block associated with the given name in the
|
|
// harness generator and expects it to be accepted, but not necessarily to the
|
|
// main chain.
|
|
func (g *chaingenHarness) AcceptBlockData(blockName string) {
|
|
g.t.Helper()
|
|
|
|
msgBlock := g.BlockByName(blockName)
|
|
blockHeight := msgBlock.Header.Height
|
|
block := dcrutil.NewBlock(msgBlock)
|
|
blockHash := block.Hash()
|
|
g.t.Logf("Testing block %q (hash %s, height %d)", blockName, blockHash,
|
|
blockHeight)
|
|
|
|
_, err := g.chain.ProcessBlock(block)
|
|
if err != nil {
|
|
g.t.Fatalf("block %q (hash %s, height %d) should have been accepted: %v",
|
|
blockName, blockHash, blockHeight, err)
|
|
}
|
|
}
|
|
|
|
// AcceptBlockDataWithExpectedTip processes the block associated with the given
|
|
// name in the harness generator and expects it to be accepted, but not
|
|
// necessarily to the main chain and for the current best chain tip to be the
|
|
// provided value.
|
|
func (g *chaingenHarness) AcceptBlockDataWithExpectedTip(blockName, tipName string) {
|
|
g.t.Helper()
|
|
|
|
g.AcceptBlockData(blockName)
|
|
g.ExpectTip(tipName)
|
|
}
|
|
|
|
// AcceptBlock processes the block associated with the given name in the
|
|
// harness generator and expects it to be accepted to the main chain.
|
|
func (g *chaingenHarness) AcceptBlock(blockName string) {
|
|
g.t.Helper()
|
|
|
|
msgBlock := g.BlockByName(blockName)
|
|
blockHeight := msgBlock.Header.Height
|
|
block := dcrutil.NewBlock(msgBlock)
|
|
g.t.Logf("Testing block %q (hash %s, height %d)", blockName, block.Hash(),
|
|
blockHeight)
|
|
|
|
forkLen, err := g.chain.ProcessBlock(block)
|
|
if err != nil {
|
|
g.t.Fatalf("block %q (hash %s, height %d) should have been accepted: %v",
|
|
blockName, block.Hash(), blockHeight, err)
|
|
}
|
|
|
|
// Ensure the block was accepted to the main chain as indicated by a fork
|
|
// length of zero.
|
|
isMainChain := forkLen == 0
|
|
if !isMainChain {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected main chain flag "+
|
|
"-- got %v, want true", blockName, block.Hash(), blockHeight,
|
|
isMainChain)
|
|
}
|
|
}
|
|
|
|
// AcceptTipBlock processes the current tip block associated with the harness
|
|
// generator and expects it to be accepted to the main chain.
|
|
func (g *chaingenHarness) AcceptTipBlock() {
|
|
g.t.Helper()
|
|
|
|
g.AcceptBlock(g.TipName())
|
|
}
|
|
|
|
// RejectHeader expects the block header associated with the given name in the
|
|
// harness generator to be rejected with the provided error kind and also
|
|
// ensures the underlying block index is consistent with the result.
|
|
func (g *chaingenHarness) RejectHeader(blockName string, kind ErrorKind) {
|
|
g.t.Helper()
|
|
|
|
header := &g.BlockByName(blockName).Header
|
|
blockHash := header.BlockHash()
|
|
blockHeight := header.Height
|
|
g.t.Logf("Testing reject block header %q (hash %s, height %d, reason %v)",
|
|
blockName, blockHash, blockHeight, kind)
|
|
|
|
// Determine if the header is already known before attempting to process it.
|
|
alreadyHaveHeader := g.chain.index.LookupNode(&blockHash) != nil
|
|
|
|
err := g.chain.ProcessBlockHeader(header)
|
|
if err == nil {
|
|
g.t.Fatalf("block header %q (hash %s, height %d) should not have been "+
|
|
"accepted", blockName, blockHash, blockHeight)
|
|
}
|
|
|
|
// Ensure the error matches the value specified in the test instance.
|
|
if !errors.Is(err, kind) {
|
|
g.t.Fatalf("block header %q (hash %s, height %d) does not have "+
|
|
"expected reject code -- got %v, want %v", blockName, blockHash,
|
|
blockHeight, err, kind)
|
|
}
|
|
|
|
// Ensure the rejected header was not added to the block index when it was
|
|
// not already previously successfully added and that it was not removed if
|
|
// it was already previously added.
|
|
node := g.chain.index.LookupNode(&blockHash)
|
|
switch {
|
|
case !alreadyHaveHeader && node == nil:
|
|
// Header was not added as expected.
|
|
return
|
|
|
|
case !alreadyHaveHeader && node != nil:
|
|
g.t.Fatalf("rejected block header %q (hash %s, height %d) was added "+
|
|
"to the block index", blockName, blockHash, blockHeight)
|
|
|
|
case alreadyHaveHeader && node == nil:
|
|
g.t.Fatalf("rejected block header %q (hash %s, height %d) was removed "+
|
|
"from the block index", blockName, blockHash, blockHeight)
|
|
}
|
|
|
|
// The header was previously added, so ensure it is not reported as having
|
|
// been validated and that it is now known invalid.
|
|
status := g.chain.index.NodeStatus(node)
|
|
if status.HasValidated() {
|
|
g.t.Fatalf("rejected block header %q (hash %s, height %d) is marked "+
|
|
"as known valid", blockName, blockHash, blockHeight)
|
|
}
|
|
if !status.KnownInvalid() {
|
|
g.t.Fatalf("rejected block header %q (hash %s, height %d) is NOT "+
|
|
"marked as known invalid", blockName, blockHash, blockHeight)
|
|
}
|
|
}
|
|
|
|
// RejectBlock expects the block associated with the given name in the harness
|
|
// generator to be rejected with the provided error kind.
|
|
func (g *chaingenHarness) RejectBlock(blockName string, kind ErrorKind) {
|
|
g.t.Helper()
|
|
|
|
msgBlock := g.BlockByName(blockName)
|
|
blockHeight := msgBlock.Header.Height
|
|
block := dcrutil.NewBlock(msgBlock)
|
|
g.t.Logf("Testing reject block %q (hash %s, height %d, reason %v)",
|
|
blockName, block.Hash(), blockHeight, kind)
|
|
|
|
_, err := g.chain.ProcessBlock(block)
|
|
if err == nil {
|
|
g.t.Fatalf("block %q (hash %s, height %d) should not have been accepted",
|
|
blockName, block.Hash(), blockHeight)
|
|
}
|
|
|
|
// Ensure the error matches the value specified in the test instance.
|
|
if !errors.Is(err, kind) {
|
|
g.t.Fatalf("block %q (hash %s, height %d) does not have expected reject "+
|
|
"code -- got %v, want %v", blockName, block.Hash(), blockHeight,
|
|
err, kind)
|
|
}
|
|
}
|
|
|
|
// RejectTipBlock expects the current tip block associated with the harness
|
|
// generator to be rejected with the provided error kind.
|
|
func (g *chaingenHarness) RejectTipBlock(kind ErrorKind) {
|
|
g.t.Helper()
|
|
|
|
g.RejectBlock(g.TipName(), kind)
|
|
}
|
|
|
|
// ExpectTip expects the provided block to be the current tip of the main chain
|
|
// associated with the harness generator.
|
|
func (g *chaingenHarness) ExpectTip(tipName string) {
|
|
g.t.Helper()
|
|
|
|
// Ensure hash and height match.
|
|
wantTip := g.BlockByName(tipName)
|
|
best := g.chain.BestSnapshot()
|
|
if best.Hash != wantTip.BlockHash() ||
|
|
best.Height != int64(wantTip.Header.Height) {
|
|
g.t.Fatalf("block %q (hash %s, height %d) should be the current tip "+
|
|
"-- got %q (hash %s, height %d)", tipName, wantTip.BlockHash(),
|
|
wantTip.Header.Height, g.BlockName(&best.Hash), best.Hash,
|
|
best.Height)
|
|
}
|
|
}
|
|
|
|
// ExpectUtxoSetState expects the provided block to be the last flushed block in
|
|
// the utxo set state in the utxo backend.
|
|
func (g *chaingenHarness) ExpectUtxoSetState(blockName string) {
|
|
g.t.Helper()
|
|
|
|
// Fetch the utxo set state from the utxo backend.
|
|
gotState, err := g.chain.utxoCache.FetchBackendState()
|
|
if err != nil {
|
|
g.t.Fatalf("unexpected error fetching utxo set state: %v", err)
|
|
}
|
|
|
|
// Validate that the state matches the expected state.
|
|
block := g.BlockByName(blockName)
|
|
wantState := &UtxoSetState{
|
|
lastFlushHeight: block.Header.Height,
|
|
lastFlushHash: block.BlockHash(),
|
|
}
|
|
if !reflect.DeepEqual(gotState, wantState) {
|
|
g.t.Fatalf("mismatched utxo set state:\nwant: %+v\n got: %+v\n", wantState,
|
|
gotState)
|
|
}
|
|
}
|
|
|
|
// AcceptedToSideChainWithExpectedTip expects the tip block associated with the
|
|
// generator to be accepted to a side chain, but the current best chain tip to
|
|
// be the provided value.
|
|
func (g *chaingenHarness) AcceptedToSideChainWithExpectedTip(tipName string) {
|
|
g.t.Helper()
|
|
|
|
msgBlock := g.Tip()
|
|
blockHeight := msgBlock.Header.Height
|
|
block := dcrutil.NewBlock(msgBlock)
|
|
g.t.Logf("Testing block %q (hash %s, height %d)", g.TipName(), block.Hash(),
|
|
blockHeight)
|
|
|
|
forkLen, err := g.chain.ProcessBlock(block)
|
|
if err != nil {
|
|
g.t.Fatalf("block %q (hash %s, height %d) should have been accepted: %v",
|
|
g.TipName(), block.Hash(), blockHeight, err)
|
|
}
|
|
|
|
// Ensure the block was accepted to a side chain as indicated by a non-zero
|
|
// fork length.
|
|
isMainChain := forkLen == 0
|
|
if isMainChain {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected main chain flag "+
|
|
"-- got %v, want false", g.TipName(), block.Hash(), blockHeight,
|
|
isMainChain)
|
|
}
|
|
|
|
g.ExpectTip(tipName)
|
|
}
|
|
|
|
// ExpectBestHeader expects the provided block header associated with the given
|
|
// name to be the one identified as the header of the chain associated with the
|
|
// harness generator with the most cumulative work that is NOT known to be
|
|
// invalid.
|
|
func (g *chaingenHarness) ExpectBestHeader(blockName string) {
|
|
g.t.Helper()
|
|
|
|
// Ensure hash and height match.
|
|
want := g.BlockByName(blockName).Header
|
|
bestHash, bestHeight := g.chain.BestHeader()
|
|
if bestHash != want.BlockHash() || bestHeight != int64(want.Height) {
|
|
g.t.Fatalf("block header %q (hash %s, height %d) should be the best "+
|
|
"known header -- got %q (hash %s, height %d)", blockName,
|
|
want.BlockHash(), want.Height, g.BlockName(&bestHash),
|
|
bestHash, bestHeight)
|
|
}
|
|
}
|
|
|
|
// ExpectBestInvalidHeader expects the provided block header associated with the
|
|
// given name to be the one identified as the header of the chain associated
|
|
// with the harness generator with the most cumulative work that is known to be
|
|
// invalid. Note that the provided block name can be an empty string to
|
|
// indicate no such header should exist.
|
|
func (g *chaingenHarness) ExpectBestInvalidHeader(blockName string) {
|
|
g.t.Helper()
|
|
|
|
bestHash := g.chain.BestInvalidHeader()
|
|
switch {
|
|
case blockName != "" && bestHash != *zeroHash:
|
|
want := g.BlockByName(blockName).Header
|
|
bestHeader := g.BlockByHash(&bestHash).Header
|
|
if bestHash != want.BlockHash() || bestHeader.Height != want.Height {
|
|
g.t.Fatalf("block header %q (hash %s, height %d) should be the "+
|
|
"best known invalid header -- got %q (hash %s, height %d)",
|
|
blockName, want.BlockHash(), want.Height, g.BlockName(&bestHash),
|
|
bestHash, bestHeader.Height)
|
|
}
|
|
|
|
case blockName != "" && bestHash == *zeroHash:
|
|
want := g.BlockByName(blockName).Header
|
|
g.t.Fatalf("block header %q (hash %s, height %d) should be the best "+
|
|
"known invalid header -- got none", blockName, want.BlockHash(),
|
|
want.Height)
|
|
|
|
case blockName == "" && bestHash != *zeroHash:
|
|
bestHeight := g.BlockByHash(&bestHash).Header.Height
|
|
g.t.Fatalf("there should not be a best known invalid header -- got %q "+
|
|
"(hash %s, height %d)", g.BlockName(&bestHash), bestHash,
|
|
bestHeight)
|
|
}
|
|
}
|
|
|
|
// ExpectIsCurrent expects the chain associated with the harness generator to
|
|
// report the given is current status.
|
|
func (g *chaingenHarness) ExpectIsCurrent(expected bool) {
|
|
g.t.Helper()
|
|
|
|
best := g.chain.BestSnapshot()
|
|
isCurrent := g.chain.IsCurrent()
|
|
if isCurrent != expected {
|
|
g.t.Fatalf("mismatched is current status for block %q (hash %s, "+
|
|
"height %d) -- got %v, want %v", g.BlockName(&best.Hash), best.Hash,
|
|
best.Height, isCurrent, expected)
|
|
}
|
|
}
|
|
|
|
// lookupDeploymentVersion returns the version of the deployment with the
|
|
// provided ID and caches the result for future invocations. An error is
|
|
// returned if the ID is not found.
|
|
func (g *chaingenHarness) lookupDeploymentVersion(voteID string) (uint32, error) {
|
|
if version, ok := g.deploymentVersions[voteID]; ok {
|
|
return version, nil
|
|
}
|
|
|
|
version, _, err := findDeployment(g.Params(), voteID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
g.deploymentVersions[voteID] = version
|
|
return version, nil
|
|
}
|
|
|
|
// TestThresholdState queries the threshold state from the current tip block
|
|
// associated with the harness generator and expects the returned state to match
|
|
// the provided value.
|
|
func (g *chaingenHarness) TestThresholdState(id string, state ThresholdState) {
|
|
g.t.Helper()
|
|
|
|
tipHash := g.Tip().BlockHash()
|
|
tipHeight := g.Tip().Header.Height
|
|
deploymentVer, err := g.lookupDeploymentVersion(id)
|
|
if err != nil {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected error when "+
|
|
"retrieving threshold state: %v", g.TipName(), tipHash, tipHeight,
|
|
err)
|
|
}
|
|
|
|
s, err := g.chain.NextThresholdState(&tipHash, deploymentVer, id)
|
|
if err != nil {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected error when "+
|
|
"retrieving threshold state: %v", g.TipName(), tipHash, tipHeight,
|
|
err)
|
|
}
|
|
|
|
if s.State != state {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected threshold "+
|
|
"state for %s -- got %v, want %v", g.TipName(), tipHash, tipHeight,
|
|
id, s.State, state)
|
|
}
|
|
}
|
|
|
|
// TestThresholdStateChoice queries the threshold state from the current tip
|
|
// block associated with the harness generator and expects the returned state
|
|
// and choice to match the provided value.
|
|
func (g *chaingenHarness) TestThresholdStateChoice(id string, state ThresholdState, choice uint32) {
|
|
g.t.Helper()
|
|
|
|
tipHash := g.Tip().BlockHash()
|
|
tipHeight := g.Tip().Header.Height
|
|
deploymentVer, err := g.lookupDeploymentVersion(id)
|
|
if err != nil {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected error when "+
|
|
"retrieving threshold state: %v", g.TipName(), tipHash, tipHeight,
|
|
err)
|
|
}
|
|
|
|
s, err := g.chain.NextThresholdState(&tipHash, deploymentVer, id)
|
|
if err != nil {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected error when "+
|
|
"retrieving threshold state: %v", g.TipName(), tipHash, tipHeight,
|
|
err)
|
|
}
|
|
|
|
if s.State != state {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected threshold "+
|
|
"state for %s -- got %v, want %v", g.TipName(), tipHash, tipHeight,
|
|
id, s.State, state)
|
|
}
|
|
if s.Choice != choice {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected choice for %s -- "+
|
|
"got %v, want %v", g.TipName(), tipHash, tipHeight, id, s.Choice,
|
|
choice)
|
|
}
|
|
}
|
|
|
|
// ForceTipReorg forces the chain instance associated with the generator to
|
|
// reorganize the current tip of the main chain from the given block to the
|
|
// given block. An error will result if the provided from block is not actually
|
|
// the current tip.
|
|
func (g *chaingenHarness) ForceTipReorg(fromTipName, toTipName string) {
|
|
g.t.Helper()
|
|
|
|
from := g.BlockByName(fromTipName)
|
|
to := g.BlockByName(toTipName)
|
|
g.t.Logf("Testing forced reorg from %q (hash %s, height %d) to %q (hash "+
|
|
"%s, height %d)", fromTipName, from.BlockHash(), from.Header.Height,
|
|
toTipName, to.BlockHash(), to.Header.Height)
|
|
|
|
err := g.chain.ForceHeadReorganization(from.BlockHash(), to.BlockHash())
|
|
if err != nil {
|
|
g.t.Fatalf("failed to force header reorg from block %q (hash %s, "+
|
|
"height %d) to block %q (hash %s, height %d): %v", fromTipName,
|
|
from.BlockHash(), from.Header.Height, toTipName, to.BlockHash(),
|
|
to.Header.Height, err)
|
|
}
|
|
}
|
|
|
|
// InvalidateBlockAndExpectTip marks the block associated with the given name in
|
|
// the harness generator as invalid and expects the provided error along with
|
|
// the resulting current best chain tip to be the block associated with the
|
|
// given tip name.
|
|
func (g *chaingenHarness) InvalidateBlockAndExpectTip(blockName string, wantErr error, tipName string) {
|
|
g.t.Helper()
|
|
|
|
msgBlock := g.BlockByName(blockName)
|
|
blockHeight := msgBlock.Header.Height
|
|
block := dcrutil.NewBlock(msgBlock)
|
|
g.t.Logf("Testing invalidate block %q (hash %s, height %d) with expected "+
|
|
"error %v", blockName, block.Hash(), blockHeight, wantErr)
|
|
|
|
err := g.chain.InvalidateBlock(block.Hash())
|
|
if !errors.Is(err, wantErr) {
|
|
g.t.Fatalf("invalidate block %q (hash %s, height %d) does not have "+
|
|
"expected error -- got %q, want %v", blockName, block.Hash(),
|
|
blockHeight, err, wantErr)
|
|
}
|
|
|
|
g.ExpectTip(tipName)
|
|
}
|
|
|
|
// ReconsiderBlockAndExpectTip reconsiders the block associated with the given
|
|
// name in the harness generator and expects the provided error along with the
|
|
// resulting current best chain tip to be the block associated with the given
|
|
// tip name.
|
|
func (g *chaingenHarness) ReconsiderBlockAndExpectTip(blockName string, wantErr error, tipName string) {
|
|
g.t.Helper()
|
|
|
|
msgBlock := g.BlockByName(blockName)
|
|
blockHeight := msgBlock.Header.Height
|
|
block := dcrutil.NewBlock(msgBlock)
|
|
|
|
g.t.Logf("Testing reconsider block %q (hash %s, height %d) with expected "+
|
|
"error %v", blockName, block.Hash(), blockHeight, wantErr)
|
|
|
|
err := g.chain.ReconsiderBlock((block.Hash()))
|
|
if !errors.Is(err, wantErr) {
|
|
g.t.Fatalf("reconsider block %q (hash %s, height %d) does not have "+
|
|
"expected error -- got %q, want %v", blockName, block.Hash(),
|
|
blockHeight, err, wantErr)
|
|
}
|
|
|
|
g.ExpectTip(tipName)
|
|
}
|
|
|
|
// minUint32 is a helper function to return the minimum of two uint32s.
|
|
// This avoids a math import and the need to cast to floats.
|
|
func minUint32(a, b uint32) uint32 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// generateToHeight generates enough blocks in the generator associated with the
|
|
// harness to reach the provided height assuming the blocks up to and including
|
|
// the provided from height have already been generated. It also purchases the
|
|
// provided number of tickets per block after coinbase maturity.
|
|
//
|
|
// The accept flag specifies whether or not to accept the blocks to the chain
|
|
// instance associated with the harness as well.
|
|
//
|
|
// The function will fail with a fatal test error if it is called with a from
|
|
// height that is greater than or equal to the to height or a number of tickets
|
|
// that exceeds the max allowed for a block.
|
|
func (g *chaingenHarness) generateToHeight(fromHeight, toHeight uint32, buyTicketsPerBlock uint32, accept bool) {
|
|
g.t.Helper()
|
|
|
|
// Only allow this to be called with sane heights.
|
|
tipHeight := fromHeight
|
|
if toHeight <= tipHeight {
|
|
g.t.Fatalf("not possible to generate to height %d when the current "+
|
|
"height is already %d", toHeight, tipHeight)
|
|
}
|
|
|
|
// Only allow this to be called with a sane number of tickets to buy per
|
|
// block.
|
|
params := g.Params()
|
|
maxOutsForTickets := uint32(params.TicketsPerBlock)
|
|
if buyTicketsPerBlock > maxOutsForTickets {
|
|
g.t.Fatalf("a max of %v outputs are available for ticket purchases "+
|
|
"per block", maxOutsForTickets)
|
|
}
|
|
|
|
// Shorter versions of useful params for convenience.
|
|
coinbaseMaturity := uint32(params.CoinbaseMaturity)
|
|
stakeEnabledHeight := uint32(params.StakeEnabledHeight)
|
|
|
|
// Add the required first block as needed.
|
|
//
|
|
// genesis -> bfb
|
|
if tipHeight == 0 {
|
|
g.CreateBlockOne("bfb", 0)
|
|
g.AssertTipHeight(1)
|
|
if accept {
|
|
g.AcceptTipBlock()
|
|
}
|
|
tipHeight++
|
|
}
|
|
intermediateHeight := uint32(1)
|
|
|
|
// Generate enough blocks to have mature coinbase outputs to work with as
|
|
// needed.
|
|
//
|
|
// genesis -> bfb -> bm2 -> bm3 -> ... -> bm#
|
|
alreadyAsserted := tipHeight >= coinbaseMaturity+1
|
|
targetHeight := minUint32(coinbaseMaturity+1, toHeight)
|
|
for ; tipHeight < targetHeight; tipHeight++ {
|
|
blockName := fmt.Sprintf("bm%d", tipHeight-intermediateHeight)
|
|
g.NextBlock(blockName, nil, nil)
|
|
g.SaveTipCoinbaseOuts()
|
|
if accept {
|
|
g.AcceptTipBlock()
|
|
}
|
|
}
|
|
intermediateHeight = targetHeight
|
|
if !alreadyAsserted {
|
|
g.AssertTipHeight(intermediateHeight)
|
|
}
|
|
|
|
// Generate enough blocks to reach the stake enabled height while creating
|
|
// ticket purchases that spend from the coinbases matured above as needed.
|
|
// This will also populate the pool of immature tickets.
|
|
//
|
|
// ... -> bm# ... -> bse18 -> bse19 -> ... -> bse#
|
|
var ticketsPurchased uint32
|
|
alreadyAsserted = tipHeight >= stakeEnabledHeight
|
|
targetHeight = minUint32(stakeEnabledHeight, toHeight)
|
|
for ; tipHeight < targetHeight; tipHeight++ {
|
|
var ticketOuts []chaingen.SpendableOut
|
|
if buyTicketsPerBlock > 0 {
|
|
// Purchase the specified number of tickets per block.
|
|
outs := g.OldestCoinbaseOuts()
|
|
ticketOuts = outs[1 : buyTicketsPerBlock+1]
|
|
ticketsPurchased += buyTicketsPerBlock
|
|
}
|
|
|
|
blockName := fmt.Sprintf("bse%d", tipHeight-intermediateHeight)
|
|
g.NextBlock(blockName, nil, ticketOuts)
|
|
g.SaveTipCoinbaseOuts()
|
|
if accept {
|
|
g.AcceptTipBlock()
|
|
}
|
|
}
|
|
intermediateHeight = targetHeight
|
|
if !alreadyAsserted {
|
|
g.AssertTipHeight(intermediateHeight)
|
|
}
|
|
|
|
targetPoolSize := uint32(g.Params().TicketPoolSize) * buyTicketsPerBlock
|
|
for ; tipHeight < toHeight; tipHeight++ {
|
|
var ticketOuts []chaingen.SpendableOut
|
|
// Only purchase tickets until the target ticket pool size is reached.
|
|
ticketsNeeded := targetPoolSize - ticketsPurchased
|
|
ticketsNeeded = minUint32(ticketsNeeded, buyTicketsPerBlock)
|
|
if ticketsNeeded > 0 {
|
|
outs := g.OldestCoinbaseOuts()
|
|
ticketOuts = outs[1 : ticketsNeeded+1]
|
|
ticketsPurchased += ticketsNeeded
|
|
}
|
|
|
|
blockName := fmt.Sprintf("bsv%d", tipHeight-intermediateHeight)
|
|
g.NextBlock(blockName, nil, ticketOuts)
|
|
g.SaveTipCoinbaseOuts()
|
|
if accept {
|
|
g.AcceptTipBlock()
|
|
}
|
|
}
|
|
g.AssertTipHeight(toHeight)
|
|
}
|
|
|
|
// AdvanceToHeight generates and accepts enough blocks to the chain instance
|
|
// associated with the harness to reach the provided height while purchasing the
|
|
// provided tickets per block after coinbase maturity.
|
|
func (g *chaingenHarness) AdvanceToHeight(height uint32, buyTicketsPerBlock uint32) {
|
|
g.t.Helper()
|
|
|
|
// Only allow this to be called with a sane height.
|
|
tipHeight := g.Tip().Header.Height
|
|
if height <= tipHeight {
|
|
g.t.Fatalf("not possible to advance to height %d when the current "+
|
|
"height is already %d", height, tipHeight)
|
|
}
|
|
|
|
const accept = true
|
|
g.generateToHeight(tipHeight, height, buyTicketsPerBlock, accept)
|
|
}
|
|
|
|
// generateToStakeValidationHeight generates enough blocks in the generator
|
|
// associated with the harness to reach stake validation height.
|
|
//
|
|
// The accept flag specifies whether or not to accept the blocks to the chain
|
|
// instance associated with the harness as well.
|
|
//
|
|
// The function will fail with a fatal test error if it is not called with the
|
|
// harness at the genesis block which is the case when it is first created.
|
|
func (g *chaingenHarness) generateToStakeValidationHeight(accept bool) {
|
|
g.t.Helper()
|
|
|
|
// Only allow this to be called on a newly created harness.
|
|
if g.Tip().Header.Height != 0 {
|
|
g.t.Fatalf("chaingen harness instance must be at the genesis block " +
|
|
"to generate to stake validation height")
|
|
}
|
|
|
|
// Shorter versions of useful params for convenience.
|
|
params := g.Params()
|
|
ticketsPerBlock := uint32(params.TicketsPerBlock)
|
|
stakeValidationHeight := uint32(params.StakeValidationHeight)
|
|
|
|
// Generate enough blocks in the associated harness generator to reach stake
|
|
// validation height.
|
|
g.generateToHeight(0, stakeValidationHeight, ticketsPerBlock, accept)
|
|
}
|
|
|
|
// GenerateToStakeValidationHeight generates enough blocks in the generator
|
|
// associated with the harness to reach stake validation height without
|
|
// accepting them to the chain instance associated with the harness.
|
|
//
|
|
// The function will fail with a fatal test error if it is not called with the
|
|
// harness at the genesis block which is the case when it is first created.
|
|
func (g *chaingenHarness) GenerateToStakeValidationHeight() {
|
|
g.t.Helper()
|
|
|
|
const accept = false
|
|
g.generateToStakeValidationHeight(accept)
|
|
}
|
|
|
|
// AdvanceToStakeValidationHeight generates and accepts enough blocks to the
|
|
// chain instance associated with the harness to reach stake validation height.
|
|
//
|
|
// The function will fail with a fatal test error if it is not called with the
|
|
// harness at the genesis block which is the case when it is first created.
|
|
func (g *chaingenHarness) AdvanceToStakeValidationHeight() {
|
|
g.t.Helper()
|
|
|
|
const accept = true
|
|
g.generateToStakeValidationHeight(accept)
|
|
}
|
|
|
|
// AdvanceFromSVHToActiveAgendas generates and accepts enough blocks with the
|
|
// appropriate vote bits set to reach one block prior to the specified agendas
|
|
// becoming active.
|
|
//
|
|
// The function will fail with a fatal test error if it is called when the
|
|
// harness is not at stake validation height or if the agendas are not all from
|
|
// the same deployment.
|
|
//
|
|
// WARNING: This function currently assumes the chain parameters were created
|
|
// via the quickVoteActivationParams. It should be updated in the future to
|
|
// work with arbitrary params.
|
|
func (g *chaingenHarness) AdvanceFromSVHToActiveAgendas(voteIDs ...string) {
|
|
g.t.Helper()
|
|
|
|
if len(voteIDs) < 1 {
|
|
g.t.Fatal("no vote IDs specified")
|
|
}
|
|
|
|
// Sum all yes bits from all the agendas.
|
|
params := g.Params()
|
|
deploymentVer, yesBits, err := findDeploymentAllYesChoices(params, voteIDs)
|
|
if err != nil {
|
|
g.t.Fatal(err)
|
|
}
|
|
|
|
// Shorter versions of useful params for convenience.
|
|
stakeValidationHeight := params.StakeValidationHeight
|
|
stakeVerInterval := params.StakeVersionInterval
|
|
ruleChangeInterval := int64(params.RuleChangeActivationInterval)
|
|
testThresholdState := func(state ThresholdState) {
|
|
for _, voteID := range voteIDs {
|
|
g.TestThresholdState(voteID, state)
|
|
}
|
|
}
|
|
|
|
// Only allow this to be called on a harness at SVH.
|
|
if g.Tip().Header.Height != uint32(stakeValidationHeight) {
|
|
g.t.Fatalf("chaingen harness instance must be at stake validation " +
|
|
"height to advance to active agendas")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Generate enough blocks to reach one block before the next two stake
|
|
// version intervals with block and vote versions for the agenda and
|
|
// stake version 0.
|
|
//
|
|
// This will result in triggering enforcement of the stake version and
|
|
// that the stake version is the deployment version. The threshold
|
|
// state for deployment will move to started since the next block also
|
|
// coincides with the start of a new rule change activation interval for
|
|
// the chosen parameters.
|
|
//
|
|
// ... -> bsv# -> bvu0 -> bvu1 -> ... -> bvu#
|
|
// ---------------------------------------------------------------------
|
|
|
|
blocksNeeded := stakeValidationHeight + stakeVerInterval*2 - 1 -
|
|
int64(g.Tip().Header.Height)
|
|
for i := int64(0); i < blocksNeeded; i++ {
|
|
outs := g.OldestCoinbaseOuts()
|
|
blockName := fmt.Sprintf("bvu%d", i)
|
|
g.NextBlock(blockName, nil, outs[1:],
|
|
chaingen.ReplaceBlockVersion(int32(deploymentVer)),
|
|
chaingen.ReplaceVoteVersions(deploymentVer))
|
|
g.SaveTipCoinbaseOuts()
|
|
g.AcceptTipBlock()
|
|
}
|
|
testThresholdState(ThresholdStarted)
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Generate enough blocks to reach the next rule change interval with
|
|
// block, stake, and vote versions for the agenda. Also, set the vote
|
|
// bits to include yes votes for the agenda.
|
|
//
|
|
// This will result in moving the threshold state for the agenda to
|
|
// locked in.
|
|
//
|
|
// ... -> bvu# -> bvli0 -> bvli1 -> ... -> bvli#
|
|
// ---------------------------------------------------------------------
|
|
|
|
blocksNeeded = stakeValidationHeight + ruleChangeInterval*2 - 1 -
|
|
int64(g.Tip().Header.Height)
|
|
for i := int64(0); i < blocksNeeded; i++ {
|
|
outs := g.OldestCoinbaseOuts()
|
|
blockName := fmt.Sprintf("bvli%d", i)
|
|
g.NextBlock(blockName, nil, outs[1:],
|
|
chaingen.ReplaceBlockVersion(int32(deploymentVer)),
|
|
chaingen.ReplaceStakeVersion(deploymentVer),
|
|
chaingen.ReplaceVotes(vbPrevBlockValid|yesBits, deploymentVer))
|
|
g.SaveTipCoinbaseOuts()
|
|
g.AcceptTipBlock()
|
|
}
|
|
g.AssertTipHeight(uint32(stakeValidationHeight + ruleChangeInterval*2 - 1))
|
|
g.AssertBlockVersion(int32(deploymentVer))
|
|
g.AssertStakeVersion(deploymentVer)
|
|
testThresholdState(ThresholdLockedIn)
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Generate enough blocks to reach the next rule change interval with
|
|
// block, stake, and vote versions for the agenda.
|
|
//
|
|
// This will result in moving the threshold state for the agenda to
|
|
// active thereby activating it.
|
|
//
|
|
// ... -> bvli# -> bva0 -> bva1 -> ... -> bva#
|
|
// ---------------------------------------------------------------------
|
|
|
|
blocksNeeded = stakeValidationHeight + ruleChangeInterval*3 - 1 -
|
|
int64(g.Tip().Header.Height)
|
|
for i := int64(0); i < blocksNeeded; i++ {
|
|
outs := g.OldestCoinbaseOuts()
|
|
blockName := fmt.Sprintf("bva%d", i)
|
|
g.NextBlock(blockName, nil, outs[1:],
|
|
chaingen.ReplaceBlockVersion(int32(deploymentVer)),
|
|
chaingen.ReplaceStakeVersion(deploymentVer),
|
|
chaingen.ReplaceVoteVersions(deploymentVer),
|
|
)
|
|
g.SaveTipCoinbaseOuts()
|
|
g.AcceptTipBlock()
|
|
}
|
|
g.AssertTipHeight(uint32(stakeValidationHeight + ruleChangeInterval*3 - 1))
|
|
g.AssertBlockVersion(int32(deploymentVer))
|
|
g.AssertStakeVersion(deploymentVer)
|
|
testThresholdState(ThresholdActive)
|
|
}
|