dcrd/blockchain/common_test.go
2021-09-16 11:10:08 -05:00

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: &paramsCopy,
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(&params.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, BFNone)
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, BFNone)
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, BFNone)
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, BFNone)
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, BFNone)
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, BFNone)
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)
}