mirror of
https://github.com/FlipsideCrypto/dcrd.git
synced 2026-02-06 10:56:47 +00:00
760 lines
26 KiB
Go
760 lines
26 KiB
Go
// Copyright (c) 2013-2016 The btcsuite developers
|
|
// Copyright (c) 2015-2019 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 (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math"
|
|
mrand "math/rand"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/decred/dcrd/blockchain/stake/v2"
|
|
"github.com/decred/dcrd/blockchain/v2/chaingen"
|
|
"github.com/decred/dcrd/chaincfg/chainhash"
|
|
"github.com/decred/dcrd/chaincfg/v2"
|
|
"github.com/decred/dcrd/database/v2"
|
|
_ "github.com/decred/dcrd/database/v2/ffldb"
|
|
"github.com/decred/dcrd/dcrutil/v2"
|
|
"github.com/decred/dcrd/txscript/v2"
|
|
"github.com/decred/dcrd/wire"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Handle memory database specially since it doesn't need the disk
|
|
// specific handling.
|
|
var db database.DB
|
|
var teardown func()
|
|
if testDbType == "memdb" {
|
|
ndb, err := database.Create(testDbType)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error creating db: %v", 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 test database.
|
|
dbPath, err := ioutil.TempDir("", dbName)
|
|
if err != nil {
|
|
err := fmt.Errorf("unable to create test db path: %v",
|
|
err)
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Create a new database to store the accepted blocks into.
|
|
ndb, err := database.Create(testDbType, dbPath, blockDataNet)
|
|
if err != nil {
|
|
os.RemoveAll(dbPath)
|
|
return nil, nil, fmt.Errorf("error creating db: %v", 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)
|
|
}
|
|
}
|
|
|
|
// Copy the chain params to ensure any modifications the tests do to
|
|
// the chain parameters do not affect the global instance.
|
|
paramsCopy := *params
|
|
|
|
// Create the main chain instance.
|
|
chain, err := New(&Config{
|
|
DB: db,
|
|
ChainParams: ¶msCopy,
|
|
TimeSource: NewMedianTime(),
|
|
SigCache: txscript.NewSigCache(1000),
|
|
})
|
|
|
|
if err != nil {
|
|
teardown()
|
|
err := fmt.Errorf("failed to create chain instance: %v", 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 | statusValid
|
|
index := newBlockIndex(nil)
|
|
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),
|
|
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. The 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 | statusValid
|
|
return node
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 deployement 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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// 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)
|
|
}
|
|
|
|
// Create a new database and chain instance to run tests against.
|
|
chain, teardownFunc, err := chainSetup(dbName, 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
|
|
}
|
|
|
|
// 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 %s (hash %s, height %d)", blockName, block.Hash(),
|
|
blockHeight)
|
|
|
|
forkLen, isOrphan, 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 main chain and orphan flags match the values specified in the
|
|
// test.
|
|
isMainChain := !isOrphan && 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)
|
|
}
|
|
if isOrphan {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected orphan flag -- "+
|
|
"got %v, want false", blockName, block.Hash(), blockHeight,
|
|
isOrphan)
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
|
|
// RejectBlock expects the block associated with the given name in the harness
|
|
// generator to be rejected with the provided error code.
|
|
func (g *chaingenHarness) RejectBlock(blockName string, code ErrorCode) {
|
|
g.t.Helper()
|
|
|
|
msgBlock := g.BlockByName(blockName)
|
|
blockHeight := msgBlock.Header.Height
|
|
block := dcrutil.NewBlock(msgBlock)
|
|
g.t.Logf("Testing block %s (hash %s, height %d)", blockName, block.Hash(),
|
|
blockHeight)
|
|
|
|
_, _, 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 code is of the expected type and the reject code matches
|
|
// the value specified in the test instance.
|
|
rerr, ok := err.(RuleError)
|
|
if !ok {
|
|
g.t.Fatalf("block %q (hash %s, height %d) returned unexpected error "+
|
|
"type -- got %T, want blockchain.RuleError", blockName,
|
|
block.Hash(), blockHeight, err)
|
|
}
|
|
if rerr.ErrorCode != code {
|
|
g.t.Fatalf("block %q (hash %s, height %d) does not have expected reject "+
|
|
"code -- got %v, want %v", blockName, block.Hash(), blockHeight,
|
|
rerr.ErrorCode, code)
|
|
}
|
|
}
|
|
|
|
// RejectTipBlock expects the current tip block associated with the harness
|
|
// generator to be rejected with the provided error code.
|
|
func (g *chaingenHarness) RejectTipBlock(code ErrorCode) {
|
|
g.t.Helper()
|
|
|
|
g.RejectBlock(g.TipName(), code)
|
|
}
|
|
|
|
// 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 (hash %s, height %d)", tipName, wantTip.BlockHash(),
|
|
wantTip.Header.Height, best.Hash, best.Height)
|
|
}
|
|
}
|
|
|
|
// 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 %s (hash %s, height %d)", g.TipName(), block.Hash(),
|
|
blockHeight)
|
|
|
|
forkLen, isOrphan, 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 main chain and orphan flags match the values specified in
|
|
// the test.
|
|
isMainChain := !isOrphan && 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)
|
|
}
|
|
if isOrphan {
|
|
g.t.Fatalf("block %q (hash %s, height %d) unexpected orphan flag -- "+
|
|
"got %v, want false", g.TipName(), block.Hash(), blockHeight,
|
|
isOrphan)
|
|
}
|
|
|
|
g.ExpectTip(tipName)
|
|
}
|
|
|
|
// 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 %s (hash %s, height %d) to %s (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)
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
// 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 advance to stake validation height")
|
|
}
|
|
|
|
// Shorter versions of useful params for convenience.
|
|
params := g.Params()
|
|
ticketsPerBlock := params.TicketsPerBlock
|
|
coinbaseMaturity := params.CoinbaseMaturity
|
|
stakeEnabledHeight := params.StakeEnabledHeight
|
|
stakeValidationHeight := params.StakeValidationHeight
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Block One.
|
|
// ---------------------------------------------------------------------
|
|
|
|
// Add the required first block.
|
|
//
|
|
// genesis -> bfb
|
|
g.CreateBlockOne("bfb", 0)
|
|
g.AssertTipHeight(1)
|
|
g.AcceptTipBlock()
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Generate enough blocks to have mature coinbase outputs to work with.
|
|
//
|
|
// genesis -> bfb -> bm0 -> bm1 -> ... -> bm#
|
|
// ---------------------------------------------------------------------
|
|
|
|
for i := uint16(0); i < coinbaseMaturity; i++ {
|
|
blockName := fmt.Sprintf("bm%d", i)
|
|
g.NextBlock(blockName, nil, nil)
|
|
g.SaveTipCoinbaseOuts()
|
|
g.AcceptTipBlock()
|
|
}
|
|
g.AssertTipHeight(uint32(coinbaseMaturity) + 1)
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Generate enough blocks to reach the stake enabled height while
|
|
// creating ticket purchases that spend from the coinbases matured
|
|
// above. This will also populate the pool of immature tickets.
|
|
//
|
|
// ... -> bm# ... -> bse0 -> bse1 -> ... -> bse#
|
|
// ---------------------------------------------------------------------
|
|
|
|
var ticketsPurchased int
|
|
for i := int64(0); int64(g.Tip().Header.Height) < stakeEnabledHeight; i++ {
|
|
outs := g.OldestCoinbaseOuts()
|
|
ticketOuts := outs[1:]
|
|
ticketsPurchased += len(ticketOuts)
|
|
blockName := fmt.Sprintf("bse%d", i)
|
|
g.NextBlock(blockName, nil, ticketOuts)
|
|
g.SaveTipCoinbaseOuts()
|
|
g.AcceptTipBlock()
|
|
}
|
|
g.AssertTipHeight(uint32(stakeEnabledHeight))
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Generate enough blocks to reach the stake validation height while
|
|
// continuing to purchase tickets using the coinbases matured above and
|
|
// allowing the immature tickets to mature and thus become live.
|
|
//
|
|
// ... -> bse# -> bsv0 -> bsv1 -> ... -> bsv#
|
|
// ---------------------------------------------------------------------
|
|
|
|
targetPoolSize := g.Params().TicketPoolSize * ticketsPerBlock
|
|
for i := int64(0); int64(g.Tip().Header.Height) < stakeValidationHeight; i++ {
|
|
// Only purchase tickets until the target ticket pool size is
|
|
// reached.
|
|
outs := g.OldestCoinbaseOuts()
|
|
ticketOuts := outs[1:]
|
|
if ticketsPurchased+len(ticketOuts) > int(targetPoolSize) {
|
|
ticketsNeeded := int(targetPoolSize) - ticketsPurchased
|
|
if ticketsNeeded > 0 {
|
|
ticketOuts = ticketOuts[1 : ticketsNeeded+1]
|
|
} else {
|
|
ticketOuts = nil
|
|
}
|
|
}
|
|
ticketsPurchased += len(ticketOuts)
|
|
|
|
blockName := fmt.Sprintf("bsv%d", i)
|
|
g.NextBlock(blockName, nil, ticketOuts)
|
|
g.SaveTipCoinbaseOuts()
|
|
g.AcceptTipBlock()
|
|
}
|
|
g.AssertTipHeight(uint32(stakeValidationHeight))
|
|
}
|
|
|
|
// AdvanceFromSVHToActiveAgenda generates and accepts enough blocks with the
|
|
// appropriate vote bits set to reach one block prior to the specified agenda
|
|
// becoming active.
|
|
//
|
|
// The function will fail with a fatal test error if it is called when the
|
|
// harness is not at stake validation height.
|
|
//
|
|
// 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) AdvanceFromSVHToActiveAgenda(voteID string) {
|
|
g.t.Helper()
|
|
|
|
// Find the correct deployment for the provided ID along with the yes
|
|
// vote choice within it.
|
|
params := g.Params()
|
|
deploymentVer, deployment, err := findDeployment(params, voteID)
|
|
if err != nil {
|
|
g.t.Fatal(err)
|
|
}
|
|
yesChoice, err := findDeploymentChoice(deployment, "yes")
|
|
if err != nil {
|
|
g.t.Fatal(err)
|
|
}
|
|
|
|
// Shorter versions of useful params for convenience.
|
|
stakeValidationHeight := params.StakeValidationHeight
|
|
stakeVerInterval := params.StakeVersionInterval
|
|
ruleChangeInterval := int64(params.RuleChangeActivationInterval)
|
|
|
|
// 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 the genesis block " +
|
|
"to advance to stake validation height")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// 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()
|
|
}
|
|
g.TestThresholdState(voteID, 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|yesChoice.Bits, deploymentVer))
|
|
g.SaveTipCoinbaseOuts()
|
|
g.AcceptTipBlock()
|
|
}
|
|
g.AssertTipHeight(uint32(stakeValidationHeight + ruleChangeInterval*2 - 1))
|
|
g.AssertBlockVersion(int32(deploymentVer))
|
|
g.AssertStakeVersion(deploymentVer)
|
|
g.TestThresholdState(voteID, 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)
|
|
g.TestThresholdState(voteID, ThresholdActive)
|
|
}
|