dcrd/blockchain/stakeversion_test.go
Dave Collins cb40146dc7
blockchain: Accept parent in blockNode constructor.
This modifies the newBlockNode function to accept the parent as an
argument to automatically connect the newly created node.  When it is
not nil, the work sum will automatically be summed and the parent of the
new node will be set accordingly.

This simplifies the block node construction a bit and allows some
redundant code to be removed.  It also paves the way for easier simpler
full block index construction in the future.
2018-02-18 20:45:22 -06:00

828 lines
22 KiB
Go

// Copyright (c) 2016-2018 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"
"testing"
"time"
"github.com/decred/dcrd/blockchain/stake"
"github.com/decred/dcrd/chaincfg"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/wire"
)
// newFakeChain returns a chain that is usable for syntetic 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.inMainChain = true
index := newBlockIndex(nil, params)
index.AddNode(node)
return &BlockChain{
chainParams: params,
deploymentCaches: newThresholdCaches(params),
bestNode: node,
index: index,
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),
}
}
// 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.
header := &wire.BlockHeader{
Version: blockVersion,
PrevBlock: parent.hash,
VoteBits: 0x01,
Bits: bits,
Height: uint32(parent.height) + 1,
Timestamp: timestamp,
StakeVersion: stakeVersion,
}
return newBlockNode(header, parent)
}
// 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,
})
}
}
func TestCalcWantHeight(t *testing.T) {
// For example, if StakeVersionInterval = 11 and StakeValidationHeight = 13 the
// windows start at 13 + (11 * 2) 25 and are as follows: 24-34, 35-45, 46-56 ...
// If height comes in at 35 we use the 24-34 window, up to height 45.
// If height comes in at 46 we use the 35-45 window, up to height 56 etc.
tests := []struct {
name string
skip int64
interval int64
multiplier int64
negative int64
}{
{
name: "13 11 10000",
skip: 13,
interval: 11,
multiplier: 10000,
},
{
name: "27 33 10000",
skip: 27,
interval: 33,
multiplier: 10000,
},
{
name: "mainnet params",
skip: chaincfg.MainNetParams.StakeValidationHeight,
interval: chaincfg.MainNetParams.StakeVersionInterval,
multiplier: 5000,
},
{
name: "testnet2 params",
skip: chaincfg.TestNet2Params.StakeValidationHeight,
interval: chaincfg.TestNet2Params.StakeVersionInterval,
multiplier: 1000,
},
{
name: "simnet params",
skip: chaincfg.SimNetParams.StakeValidationHeight,
interval: chaincfg.SimNetParams.StakeVersionInterval,
multiplier: 10000,
},
{
name: "negative mainnet params",
skip: chaincfg.MainNetParams.StakeValidationHeight,
interval: chaincfg.MainNetParams.StakeVersionInterval,
multiplier: 1000,
negative: 1,
},
}
for _, test := range tests {
t.Logf("running: %v skip: %v interval: %v",
test.name, test.skip, test.interval)
start := int64(test.skip + test.interval*2)
expectedHeight := start - 1 // zero based
x := int64(0) + test.negative
for i := start; i < test.multiplier*test.interval; i++ {
if x%test.interval == 0 && i != start {
expectedHeight += test.interval
}
wantHeight := calcWantHeight(test.skip, test.interval, i)
if wantHeight != expectedHeight {
if test.negative == 0 {
t.Fatalf("%v: i %v x %v -> wantHeight %v expectedHeight %v\n",
test.name, i, x, wantHeight, expectedHeight)
}
}
x++
}
}
}
// TestCalcStakeVersionCorners ensures that stake version calculation works as
// intended under various corner cases such as attempting to go back backwards.
func TestCalcStakeVersionCorners(t *testing.T) {
params := &chaincfg.SimNetParams
svh := params.StakeValidationHeight
svi := params.StakeVersionInterval
// Generate enough nodes to reach stake validation height with stake
// versions set to 0.
bc := newFakeChain(params)
node := bc.bestNode
for i := int64(1); i <= svh; i++ {
node = newFakeNode(node, 0, 0, 0, time.Now())
bc.bestNode = node
}
if node.height != svh {
t.Fatalf("invalid height got %v expected %v", node.height, svh)
}
// Generate 3 intervals with v2 votes and calculated stake version.
for i := int64(0); i < svi*3; i++ {
sv, err := bc.calcStakeVersionByNode(node)
if err != nil {
t.Fatalf("calcStakeVersionByNode: unexpected error: %v", err)
}
// Set vote and stake versions.
node = newFakeNode(node, 3, sv, 0, time.Now())
appendFakeVotes(node, params.TicketsPerBlock, 2, 0)
bc.bestNode = node
}
// Versions 0 and 2 should now be considered the majority version, but
// v4 should not yet be considered majority.
if !bc.isStakeMajorityVersion(0, node) {
t.Fatalf("invalid StakeVersion expected 0 -> true")
}
if !bc.isStakeMajorityVersion(2, node) {
t.Fatalf("invalid StakeVersion expected 2 -> true")
}
if bc.isStakeMajorityVersion(4, node) {
t.Fatalf("invalid StakeVersion expected 4 -> false")
}
// Generate 3 intervals with v4 votes and calculated stake version.
for i := int64(0); i < svi*3; i++ {
sv, err := bc.calcStakeVersionByNode(node)
if err != nil {
t.Fatalf("calcStakeVersionByNode: unexpected error: %v", err)
}
// Set vote and stake versions.
node = newFakeNode(node, 3, sv, 0, time.Now())
appendFakeVotes(node, params.TicketsPerBlock, 4, 0)
bc.bestNode = node
}
// Versions up to and including v4 should now be considered the majority
// version, but v5 should not yet be considered majority.
for _, version := range []uint32{0, 2, 4} {
if !bc.isStakeMajorityVersion(version, node) {
t.Fatalf("invalid StakeVersion expected %d -> true",
version)
}
}
if bc.isStakeMajorityVersion(5, node) {
t.Fatalf("invalid StakeVersion expected 5 -> false")
}
// Generate 3 intervals with v2 votes and calculated stake version.
for i := int64(0); i < svi*3; i++ {
sv, err := bc.calcStakeVersionByNode(node)
if err != nil {
t.Fatalf("calcStakeVersionByNode: unexpected error: %v", err)
}
// Set vote and stake versions.
node = newFakeNode(node, 3, sv, 0, time.Now())
appendFakeVotes(node, params.TicketsPerBlock, 2, 0)
bc.bestNode = node
}
// Versions up to and including v4 should still be considered the
// majority version since even though there were multiple intervals with
// a majority v2 votes, the stake version is not allowed to go
// backwards. Version 5 should still not be consider majority.
for _, version := range []uint32{0, 2, 4} {
if !bc.isStakeMajorityVersion(version, node) {
t.Fatalf("invalid StakeVersion expected %d -> true",
version)
}
}
if bc.isStakeMajorityVersion(5, node) {
t.Fatalf("invalid StakeVersion expected 5 -> false")
}
// Generate 2 intervals with v5 votes and calculated stake version.
for i := int64(0); i < svi*2; i++ {
sv, err := bc.calcStakeVersionByNode(node)
if err != nil {
t.Fatalf("calcStakeVersionByNode: unexpected error: %v", err)
}
// Set vote and stake versions.
node = newFakeNode(node, 3, sv, 0, time.Now())
appendFakeVotes(node, params.TicketsPerBlock, 5, 0)
bc.bestNode = node
}
// Versions up to and including v5 should now be considered the majority
// version, but v6 should not yet be.
for _, version := range []uint32{0, 2, 4, 5} {
if !bc.isStakeMajorityVersion(version, node) {
t.Fatalf("invalid StakeVersion expected %d -> true",
version)
}
}
if bc.isStakeMajorityVersion(6, node) {
t.Fatalf("invalid StakeVersion expected 6 -> false")
}
// Generate 1 interval with v4 votes to test the edge condition.
for i := int64(0); i < svi; i++ {
sv, err := bc.calcStakeVersionByNode(node)
if err != nil {
t.Fatalf("calcStakeVersionByNode: unexpected error: %v", err)
}
// Set vote and stake versions.
node = newFakeNode(node, 3, sv, 0, time.Now())
appendFakeVotes(node, params.TicketsPerBlock, 4, 0)
bc.bestNode = node
}
// Versions up to and including v5 should still be considered the
// majority version since even though there was an interval with a
// majority v4 votes, the stake version is not allowed to go backwards.
// Version 6 should still not be consider majority.
for _, version := range []uint32{0, 2, 4, 5} {
if !bc.isStakeMajorityVersion(version, node) {
t.Fatalf("invalid StakeVersion expected %d -> true",
version)
}
}
if bc.isStakeMajorityVersion(6, node) {
t.Fatalf("invalid StakeVersion expected 6 -> false")
}
// Generate another interval with v4 votes.
for i := int64(0); i < svi; i++ {
sv, err := bc.calcStakeVersionByNode(node)
if err != nil {
t.Fatalf("calcStakeVersionByNode: unexpected error: %v", err)
}
// Set stake versions.
node = newFakeNode(node, 3, sv, 0, time.Now())
appendFakeVotes(node, params.TicketsPerBlock, 4, 0)
bc.bestNode = node
}
// Versions up to and including v5 should still be considered the
// majority version since even though there was another interval with a
// majority v4 votes, the stake version is not allowed to go backwards.
// Version 6 should still not be consider majority.
for _, version := range []uint32{0, 2, 4, 5} {
if !bc.isStakeMajorityVersion(version, node) {
t.Fatalf("invalid StakeVersion expected %d -> true",
version)
}
}
if bc.isStakeMajorityVersion(6, node) {
t.Fatalf("invalid StakeVersion expected 6 -> false")
}
}
// TestCalcStakeVersionByNode ensures that stake version calculation works as
// intended when
func TestCalcStakeVersionByNode(t *testing.T) {
params := &chaincfg.SimNetParams
svh := params.StakeValidationHeight
svi := params.StakeVersionInterval
tpb := params.TicketsPerBlock
tests := []struct {
name string
numNodes int64
expectVersion uint32
set func(*blockNode)
}{
{
name: "headerStake 2 votes 3",
numNodes: svh + svi*3,
expectVersion: 3,
set: func(node *blockNode) {
if int64(node.height) > svh {
appendFakeVotes(node, tpb, 3, 0)
node.stakeVersion = 2
node.blockVersion = 3
}
},
},
{
name: "headerStake 3 votes 2",
numNodes: svh + svi*3,
expectVersion: 3,
set: func(node *blockNode) {
if int64(node.height) > svh {
appendFakeVotes(node, tpb, 2, 0)
node.stakeVersion = 3
node.blockVersion = 3
}
},
},
}
for _, test := range tests {
bc := newFakeChain(params)
node := bc.bestNode
for i := int64(1); i <= test.numNodes; i++ {
node = newFakeNode(node, 1, 0, 0, time.Now())
test.set(node)
bc.bestNode = node
}
version, err := bc.calcStakeVersionByNode(bc.bestNode)
if err != nil {
t.Fatalf("calcStakeVersionByNode: unexpected error: %v",
err)
}
if version != test.expectVersion {
t.Fatalf("version mismatch: got %v expected %v",
version, test.expectVersion)
}
}
}
// TestIsStakeMajorityVersion ensures that determining the current majority
// stake version works as intended under a wide variety of scenarios.
func TestIsStakeMajorityVersion(t *testing.T) {
params := &chaincfg.MainNetParams
svh := params.StakeValidationHeight
svi := params.StakeVersionInterval
tpb := params.TicketsPerBlock
// Calculate super majority for 5 and 3 ticket maxes.
maxTickets5 := int32(svi) * int32(tpb)
sm5 := maxTickets5 * params.StakeMajorityMultiplier / params.StakeMajorityDivisor
maxTickets3 := int32(svi) * int32(tpb-2)
sm3 := maxTickets3 * params.StakeMajorityMultiplier / params.StakeMajorityDivisor
// Keep track of ticketcount in set. Must be reset every test.
ticketCount := int32(0)
tests := []struct {
name string
numNodes int64
set func(*blockNode)
blockVersion int32
startStakeVersion uint32
expectedStakeVersion uint32
expectedCalcVersion uint32
result bool
}{
{
name: "too shallow",
numNodes: svh + svi - 1,
startStakeVersion: 1,
expectedStakeVersion: 1,
expectedCalcVersion: 0,
result: true,
},
{
name: "just enough",
numNodes: svh + svi,
startStakeVersion: 1,
expectedStakeVersion: 1,
expectedCalcVersion: 0,
result: true,
},
{
name: "odd",
numNodes: svh + svi + 1,
startStakeVersion: 1,
expectedStakeVersion: 1,
expectedCalcVersion: 0,
result: true,
},
{
name: "100%",
numNodes: svh + svi,
set: func(node *blockNode) {
if int64(node.height) > svh {
appendFakeVotes(node, tpb, 2, 0)
}
},
startStakeVersion: 1,
expectedStakeVersion: 2,
expectedCalcVersion: 0,
result: true,
},
{
name: "50%",
numNodes: svh + (svi * 2),
set: func(node *blockNode) {
if int64(node.height) <= svh {
return
}
if int64(node.height) < svh+svi {
appendFakeVotes(node, tpb, 1, 0)
return
}
threshold := maxTickets5 / 2
v := uint32(1)
for i := 0; i < int(tpb); i++ {
if ticketCount >= threshold {
v = 2
}
appendFakeVotes(node, 1, v, 0)
ticketCount++
}
},
startStakeVersion: 1,
expectedStakeVersion: 2,
expectedCalcVersion: 0,
result: false,
},
{
name: "75%-1",
numNodes: svh + (svi * 2),
set: func(node *blockNode) {
if int64(node.height) < svh {
return
}
if int64(node.height) < svh+svi {
appendFakeVotes(node, tpb, 1, 0)
return
}
threshold := maxTickets5 - sm5 + 1
v := uint32(1)
for i := 0; i < int(tpb); i++ {
if ticketCount >= threshold {
v = 2
}
appendFakeVotes(node, 1, v, 0)
ticketCount++
}
},
startStakeVersion: 1,
expectedStakeVersion: 2,
expectedCalcVersion: 0,
result: false,
},
{
name: "75%",
numNodes: svh + (svi * 2),
set: func(node *blockNode) {
if int64(node.height) <= svh {
return
}
if int64(node.height) < svh+svi {
appendFakeVotes(node, tpb, 1, 0)
return
}
threshold := maxTickets5 - sm5
v := uint32(1)
for i := 0; i < int(tpb); i++ {
if ticketCount >= threshold {
v = 2
}
appendFakeVotes(node, 1, v, 0)
ticketCount++
}
},
startStakeVersion: 1,
expectedStakeVersion: 2,
expectedCalcVersion: 0,
result: true,
},
{
name: "100% after several non majority intervals",
numNodes: svh + (params.StakeVersionInterval * 222),
set: func(node *blockNode) {
if int64(node.height) <= svh {
return
}
if int64(node.height) < svh+svi {
appendFakeVotes(node, tpb, 1, 0)
return
}
for i := uint32(0); i < uint32(tpb); i++ {
appendFakeVotes(node, 1, i%5, 0)
}
},
startStakeVersion: 1,
expectedStakeVersion: 1,
expectedCalcVersion: 0,
result: true,
},
{
name: "no majority ever",
numNodes: svh + (svi * 8),
set: func(node *blockNode) {
if int64(node.height) <= svh {
return
}
for i := uint32(0); i < uint32(tpb); i++ {
appendFakeVotes(node, 1, i%5, 0)
}
},
startStakeVersion: 1,
expectedStakeVersion: 1,
expectedCalcVersion: 0,
result: true,
},
{
name: "75%-1 with 3 votes",
numNodes: svh + (svi * 2),
set: func(node *blockNode) {
if int64(node.height) < svh {
return
}
if int64(node.height) < svh+svi {
appendFakeVotes(node, tpb-2, 1, 0)
return
}
threshold := maxTickets3 - sm3 + 1
v := uint32(1)
for i := 0; i < int(tpb-2); i++ {
if ticketCount >= threshold {
v = 2
}
appendFakeVotes(node, 1, v, 0)
ticketCount++
}
},
startStakeVersion: 1,
expectedStakeVersion: 2,
expectedCalcVersion: 0,
result: false,
},
{
name: "75% with 3 votes",
numNodes: svh + (svi * 2),
set: func(node *blockNode) {
if int64(node.height) <= svh {
return
}
if int64(node.height) < svh+svi {
appendFakeVotes(node, tpb-2, 1, 0)
return
}
threshold := maxTickets3 - sm3
v := uint32(1)
for i := 0; i < int(tpb-2); i++ {
if ticketCount >= threshold {
v = 2
}
appendFakeVotes(node, 1, v, 0)
ticketCount++
}
},
startStakeVersion: 1,
expectedStakeVersion: 2,
expectedCalcVersion: 0,
result: true,
},
{
name: "75% with 3 votes blockversion 3",
numNodes: svh + (svi * 2),
set: func(node *blockNode) {
if int64(node.height) <= svh {
return
}
if int64(node.height) < svh+svi {
appendFakeVotes(node, tpb-2, 1, 0)
return
}
threshold := maxTickets3 - sm3
v := uint32(1)
for i := 0; i < int(tpb-2); i++ {
if ticketCount >= threshold {
v = 2
}
appendFakeVotes(node, 1, v, 0)
ticketCount++
}
},
blockVersion: 3,
startStakeVersion: 1,
expectedStakeVersion: 2,
expectedCalcVersion: 2,
result: true,
},
{
name: "75%-1 with 3 votes blockversion 3",
numNodes: svh + (svi * 2),
set: func(node *blockNode) {
if int64(node.height) < svh {
return
}
if int64(node.height) < svh+svi {
appendFakeVotes(node, tpb-2, 1, 0)
return
}
threshold := maxTickets3 - sm3 + 1
v := uint32(1)
for i := 0; i < int(tpb-2); i++ {
if ticketCount >= threshold {
v = 2
}
appendFakeVotes(node, 1, v, 0)
ticketCount++
}
},
blockVersion: 3,
startStakeVersion: 1,
expectedStakeVersion: 2,
expectedCalcVersion: 1,
result: false,
},
}
for _, test := range tests {
// Create new BlockChain in order to blow away cache.
bc := newFakeChain(params)
node := bc.bestNode
node.stakeVersion = test.startStakeVersion
ticketCount = 0
for i := int64(1); i <= test.numNodes; i++ {
node = newFakeNode(node, test.blockVersion,
test.startStakeVersion, 0, time.Now())
// Override version.
if test.set != nil {
test.set(node)
} else {
appendFakeVotes(node, tpb,
test.startStakeVersion, 0)
}
bc.bestNode = node
}
res := bc.isVoterMajorityVersion(test.expectedStakeVersion, node)
if res != test.result {
t.Fatalf("%v isVoterMajorityVersion", test.name)
}
// validate calcStakeVersion
version, err := bc.calcStakeVersionByNode(node)
if err != nil {
t.Fatalf("calcStakeVersionByNode: unexpected error: %v", err)
}
if version != test.expectedCalcVersion {
t.Fatalf("%v calcStakeVersionByNode got %v expected %v",
test.name, version, test.expectedCalcVersion)
}
}
}
func TestLarge(t *testing.T) {
params := &chaincfg.MainNetParams
numRuns := 5
numBlocks := params.StakeVersionInterval * 100
numBlocksShallow := params.StakeVersionInterval * 10
tests := []struct {
name string
numNodes int64
blockVersion int32
startStakeVersion uint32
expectedStakeVersion uint32
expectedCalcVersion uint32
result bool
}{
{
name: "shallow cache",
numNodes: numBlocksShallow,
startStakeVersion: 1,
expectedStakeVersion: 1,
expectedCalcVersion: 0,
result: true,
},
{
name: "deep cache",
numNodes: numBlocks,
startStakeVersion: 1,
expectedStakeVersion: 1,
expectedCalcVersion: 0,
result: true,
},
}
for _, test := range tests {
// Create new BlockChain in order to blow away cache.
bc := newFakeChain(params)
node := bc.bestNode
node.stakeVersion = test.startStakeVersion
for i := int64(1); i <= test.numNodes; i++ {
node = newFakeNode(node, test.blockVersion,
test.startStakeVersion, 0, time.Now())
// Override version.
appendFakeVotes(node, params.TicketsPerBlock,
test.startStakeVersion, 0)
bc.bestNode = node
}
for x := 0; x < numRuns; x++ {
start := time.Now()
res := bc.isVoterMajorityVersion(test.expectedStakeVersion, node)
if res != test.result {
t.Fatalf("%v isVoterMajorityVersion got %v expected %v", test.name, res, test.result)
}
// validate calcStakeVersion
version, err := bc.calcStakeVersionByNode(node)
if err != nil {
t.Fatalf("calcStakeVersionByNode: unexpected error: %v", err)
}
if version != test.expectedCalcVersion {
t.Fatalf("%v calcStakeVersionByNode got %v expected %v",
test.name, version, test.expectedCalcVersion)
}
end := time.Now()
setup := "setup 0"
if x != 0 {
setup = fmt.Sprintf("run %v", x)
}
vkey := stakeMajorityCacheKeySize + 8 // bool on x86_64
key := chainhash.HashSize + 4 // size of uint32
cost := len(bc.isVoterMajorityVersionCache) * vkey
cost += len(bc.isStakeMajorityVersionCache) * vkey
cost += len(bc.calcPriorStakeVersionCache) * key
cost += len(bc.calcVoterVersionIntervalCache) * key
cost += len(bc.calcStakeVersionCache) * key
memoryCost := fmt.Sprintf("memory cost: %v", cost)
t.Logf("run time (%v) %v %v", setup, end.Sub(start),
memoryCost)
}
}
}