dcrd/blockchain/stakeversion_test.go
Dave Collins 8c69c29d69
blockchain: Consolidate tests into the main package.
Putting the test code in the same package makes it easier for forks
since they don't have to change the import paths as much and it also
gets rid of the need for internal_test.go to bridge.
2018-02-25 19:01:57 -06:00

746 lines
19 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/chaincfg"
"github.com/decred/dcrd/chaincfg/chainhash"
)
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 := bc.calcStakeVersion(node)
// 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 := bc.calcStakeVersion(node)
// 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 := bc.calcStakeVersion(node)
// 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 := bc.calcStakeVersion(node)
// 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 := bc.calcStakeVersion(node)
// 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 := bc.calcStakeVersion(node)
// 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 := bc.calcStakeVersion(bc.bestNode)
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 := bc.calcStakeVersion(node)
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 := bc.calcStakeVersion(node)
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)
}
}
}