btcd/blockchain/thresholdstate_test.go
Olaoluwa Osuntokun 54f6fa948e
blockchain: refactor new thresholdState method, test BIP9 transitions
In this commit, we extract the BIP 9 state transition logic from the
thresholdState method into a new thresholdStateTransition function that
allows us to test all the defined state transitions, including the
modified "speedy trial" logic.
2022-01-25 15:27:48 -08:00

319 lines
7.7 KiB
Go

// Copyright (c) 2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package blockchain
import (
"testing"
"github.com/btcsuite/btcd/chaincfg/chainhash"
)
// TestThresholdStateStringer tests the stringized output for the
// ThresholdState type.
func TestThresholdStateStringer(t *testing.T) {
t.Parallel()
tests := []struct {
in ThresholdState
want string
}{
{ThresholdDefined, "ThresholdDefined"},
{ThresholdStarted, "ThresholdStarted"},
{ThresholdLockedIn, "ThresholdLockedIn"},
{ThresholdActive, "ThresholdActive"},
{ThresholdFailed, "ThresholdFailed"},
{0xff, "Unknown ThresholdState (255)"},
}
// Detect additional threshold states that don't have the stringer added.
if len(tests)-1 != int(numThresholdsStates) {
t.Errorf("It appears a threshold statewas added without " +
"adding an associated stringer test")
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.String()
if result != test.want {
t.Errorf("String #%d\n got: %s want: %s", i, result,
test.want)
continue
}
}
}
// TestThresholdStateCache ensure the threshold state cache works as intended
// including adding entries, updating existing entries, and flushing.
func TestThresholdStateCache(t *testing.T) {
t.Parallel()
tests := []struct {
name string
numEntries int
state ThresholdState
}{
{name: "2 entries defined", numEntries: 2, state: ThresholdDefined},
{name: "7 entries started", numEntries: 7, state: ThresholdStarted},
{name: "10 entries active", numEntries: 10, state: ThresholdActive},
{name: "5 entries locked in", numEntries: 5, state: ThresholdLockedIn},
{name: "3 entries failed", numEntries: 3, state: ThresholdFailed},
}
nextTest:
for _, test := range tests {
cache := &newThresholdCaches(1)[0]
for i := 0; i < test.numEntries; i++ {
var hash chainhash.Hash
hash[0] = uint8(i + 1)
// Ensure the hash isn't available in the cache already.
_, ok := cache.Lookup(&hash)
if ok {
t.Errorf("Lookup (%s): has entry for hash %v",
test.name, hash)
continue nextTest
}
// Ensure hash that was added to the cache reports it's
// available and the state is the expected value.
cache.Update(&hash, test.state)
state, ok := cache.Lookup(&hash)
if !ok {
t.Errorf("Lookup (%s): missing entry for hash "+
"%v", test.name, hash)
continue nextTest
}
if state != test.state {
t.Errorf("Lookup (%s): state mismatch - got "+
"%v, want %v", test.name, state,
test.state)
continue nextTest
}
// Ensure adding an existing hash with the same state
// doesn't break the existing entry.
cache.Update(&hash, test.state)
state, ok = cache.Lookup(&hash)
if !ok {
t.Errorf("Lookup (%s): missing entry after "+
"second add for hash %v", test.name,
hash)
continue nextTest
}
if state != test.state {
t.Errorf("Lookup (%s): state mismatch after "+
"second add - got %v, want %v",
test.name, state, test.state)
continue nextTest
}
// Ensure adding an existing hash with a different state
// updates the existing entry.
newState := ThresholdFailed
if newState == test.state {
newState = ThresholdStarted
}
cache.Update(&hash, newState)
state, ok = cache.Lookup(&hash)
if !ok {
t.Errorf("Lookup (%s): missing entry after "+
"state change for hash %v", test.name,
hash)
continue nextTest
}
if state != newState {
t.Errorf("Lookup (%s): state mismatch after "+
"state change - got %v, want %v",
test.name, state, newState)
continue nextTest
}
}
}
}
type customDeploymentChecker struct {
started bool
ended bool
eligible bool
isSpeedy bool
conditionTrue bool
activationThreshold uint32
minerWindow uint32
}
func (c customDeploymentChecker) HasStarted(_ *blockNode) bool {
return c.started
}
func (c customDeploymentChecker) HasEnded(_ *blockNode) bool {
return c.ended
}
func (c customDeploymentChecker) RuleChangeActivationThreshold() uint32 {
return c.activationThreshold
}
func (c customDeploymentChecker) MinerConfirmationWindow() uint32 {
return c.minerWindow
}
func (c customDeploymentChecker) EligibleToActivate(_ *blockNode) bool {
return c.eligible
}
func (c customDeploymentChecker) IsSpeedy() bool {
return c.isSpeedy
}
func (c customDeploymentChecker) Condition(_ *blockNode) (bool, error) {
return c.conditionTrue, nil
}
// TestThresholdStateTransition tests that the thresholdStateTransition
// properly implements the BIP 009 state machine, along with the speedy trial
// augments.
func TestThresholdStateTransition(t *testing.T) {
t.Parallel()
// Prev node always points back to itself, effectively creating an
// infinite chain for the purposes of this test.
prevNode := &blockNode{}
prevNode.parent = prevNode
window := int32(2016)
testCases := []struct {
currentState ThresholdState
nextState ThresholdState
checker thresholdConditionChecker
}{
// From defined, we stay there if we haven't started the
// window, and the window hasn't ended.
{
currentState: ThresholdDefined,
nextState: ThresholdDefined,
checker: &customDeploymentChecker{},
},
// From defined, we go to failed if the window has ended, and
// this isn't a speedy trial.
{
currentState: ThresholdDefined,
nextState: ThresholdFailed,
checker: &customDeploymentChecker{
ended: true,
},
},
// From defined, even if the window has ended, we go to started
// if this isn't a speedy trial.
{
currentState: ThresholdDefined,
nextState: ThresholdStarted,
checker: &customDeploymentChecker{
started: true,
},
},
// From started, we go to failed if this isn't speed, and the
// deployment has ended.
{
currentState: ThresholdStarted,
nextState: ThresholdFailed,
checker: &customDeploymentChecker{
ended: true,
},
},
// From started, we go to locked in if the window passed the
// condition.
{
currentState: ThresholdStarted,
nextState: ThresholdLockedIn,
checker: &customDeploymentChecker{
started: true,
conditionTrue: true,
},
},
// From started, we go to failed if this is a speedy trial, and
// the condition wasn't met in the window.
{
currentState: ThresholdStarted,
nextState: ThresholdFailed,
checker: &customDeploymentChecker{
started: true,
ended: true,
isSpeedy: true,
conditionTrue: false,
activationThreshold: 1815,
},
},
// From locked in, we go straight to active is this isn't a
// speedy trial.
{
currentState: ThresholdLockedIn,
nextState: ThresholdActive,
checker: &customDeploymentChecker{
eligible: true,
},
},
// From locked in, we remain in locked in if we're not yet
// eligible to activate.
{
currentState: ThresholdLockedIn,
nextState: ThresholdLockedIn,
checker: &customDeploymentChecker{},
},
// From active, we always stay here.
{
currentState: ThresholdActive,
nextState: ThresholdActive,
checker: &customDeploymentChecker{},
},
// From failed, we always stay here.
{
currentState: ThresholdFailed,
nextState: ThresholdFailed,
checker: &customDeploymentChecker{},
},
}
for i, testCase := range testCases {
nextState, err := thresholdStateTransition(
testCase.currentState, prevNode, testCase.checker,
window,
)
if err != nil {
t.Fatalf("#%v: unable to transition to next "+
"state: %v", i, err)
}
if nextState != testCase.nextState {
t.Fatalf("#%v: incorrect state transition: "+
"expected %v got %v", i, testCase.nextState,
nextState)
}
}
}