Skip to content

Commit

Permalink
blockchain: refactor new thresholdState method, test BIP9 transitions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Roasbeef committed Jan 25, 2022
1 parent c6b66ee commit 54f6fa9
Show file tree
Hide file tree
Showing 2 changed files with 285 additions and 81 deletions.
182 changes: 101 additions & 81 deletions blockchain/thresholdstate.go
Expand Up @@ -156,6 +156,99 @@ func (b *BlockChain) PastMedianTime(blockHeader *wire.BlockHeader) (time.Time, e
return blockNode.CalcPastMedianTime(), nil
}

// thresholdStateTransition given a state, a previous node, and a toeholds
// checker, this function transitions to the next state as defined by BIP 009.
// This state transition function is also aware of the "speedy trial"
// modifications made to BIP 0009 as part of the taproot softfork activation.
func thresholdStateTransition(state ThresholdState, prevNode *blockNode,
checker thresholdConditionChecker,
confirmationWindow int32) (ThresholdState, error) {

switch state {
case ThresholdDefined:
// The deployment of the rule change fails if it
// expires before it is accepted and locked in. However
// speed deployments can only transition to failed
// after a confirmation window.
if !checker.IsSpeedy() && checker.HasEnded(prevNode) {
state = ThresholdFailed
break
}

// The state for the rule moves to the started state
// once its start time has been reached (and it hasn't
// already expired per the above).
if checker.HasStarted(prevNode) {
state = ThresholdStarted
}

case ThresholdStarted:
// The deployment of the rule change fails if it
// expires before it is accepted and locked in, but
// only if this deployment isn't speedy.
if !checker.IsSpeedy() && checker.HasEnded(prevNode) {
state = ThresholdFailed
break
}

// At this point, the rule change is still being voted
// on by the miners, so iterate backwards through the
// confirmation window to count all of the votes in it.
var count uint32
countNode := prevNode
for i := int32(0); i < confirmationWindow; i++ {
condition, err := checker.Condition(countNode)
if err != nil {
return ThresholdFailed, err
}
if condition {
count++
}

// Get the previous block node.
countNode = countNode.parent
}

switch {
// The state is locked in if the number of blocks in the
// period that voted for the rule change meets the
// activation threshold.
case count >= checker.RuleChangeActivationThreshold():
state = ThresholdLockedIn

// If this is a speedy deployment, we didn't meet the
// threshold above, and the deployment has expired, then
// we transition to failed.
case checker.IsSpeedy() && checker.HasEnded(prevNode):
state = ThresholdFailed
}

case ThresholdLockedIn:
// At this point, we'll consult the deployment see if a
// custom deployment has any other arbitrary conditions
// that need to pass before execution. This might be a
// minimum activation height or another policy.
//
// If we aren't eligible to active yet, then we'll just
// stay in the locked in position.
if !checker.EligibleToActivate(prevNode) {
state = ThresholdLockedIn
} else {
// The new rule becomes active when its
// previous state was locked in assuming it's
// now eligible to activate.
state = ThresholdActive
}

// Nothing to do if the previous state is active or failed since
// they are both terminal states.
case ThresholdActive:
case ThresholdFailed:
}

return state, nil
}

// thresholdState returns the current rule change threshold state for the block
// AFTER the given node and deployment ID. The cache is used to ensure the
// threshold states for previous windows are only calculated once.
Expand Down Expand Up @@ -216,90 +309,17 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit

// Since each threshold state depends on the state of the previous
// window, iterate starting from the oldest unknown window.
var err error
for neededNum := len(neededStates) - 1; neededNum >= 0; neededNum-- {
prevNode := neededStates[neededNum]

switch state {
case ThresholdDefined:
// The deployment of the rule change fails if it
// expires before it is accepted and locked in. However
// speed deployments can only transition to failed
// after a confirmation window.
if !checker.IsSpeedy() && checker.HasEnded(prevNode) {
state = ThresholdFailed
break
}

// The state for the rule moves to the started state
// once its start time has been reached (and it hasn't
// already expired per the above).
if checker.HasStarted(prevNode) {
state = ThresholdStarted
}

case ThresholdStarted:
// The deployment of the rule change fails if it
// expires before it is accepted and locked in, but
// only if this deployment isn't speedy.
if !checker.IsSpeedy() && checker.HasEnded(prevNode) {
state = ThresholdFailed
break
}

// At this point, the rule change is still being voted
// on by the miners, so iterate backwards through the
// confirmation window to count all of the votes in it.
var count uint32
countNode := prevNode
for i := int32(0); i < confirmationWindow; i++ {
condition, err := checker.Condition(countNode)
if err != nil {
return ThresholdFailed, err
}
if condition {
count++
}

// Get the previous block node.
countNode = countNode.parent
}

switch {
// The state is locked in if the number of blocks in the
// period that voted for the rule change meets the
// activation threshold.
case count >= checker.RuleChangeActivationThreshold():
state = ThresholdLockedIn

// If this is a speedy deployment, we didn't meet the
// threshold above, and the deployment has expired, then
// we transition to failed.
case checker.IsSpeedy() && checker.HasEnded(prevNode):
state = ThresholdFailed
}

case ThresholdLockedIn:
// At this point, we'll consult the deployment see if a
// custom deployment has any other arbitrary conditions
// that need to pass before execution. This might be a
// minimum activation height or another policy.
//
// If we aren't eligible to active yet, then we'll just
// stay in the locked in position.
if !checker.EligibleToActivate(prevNode) {
state = ThresholdLockedIn

} else {
// The new rule becomes active when its
// previous state was locked in assuming it's
// now eligible to activate.
state = ThresholdActive
}

// Nothing to do if the previous state is active or failed since
// they are both terminal states.
case ThresholdActive:
case ThresholdFailed:
// Based on the current state, the previous node, and the
// condition checker, transition to the next threshold state.
state, err = thresholdStateTransition(
state, prevNode, checker, confirmationWindow,
)
if err != nil {
return state, err
}

// Update the cache to avoid recalculating the state in the
Expand Down
184 changes: 184 additions & 0 deletions blockchain/thresholdstate_test.go
Expand Up @@ -132,3 +132,187 @@ 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)
}
}
}

0 comments on commit 54f6fa9

Please sign in to comment.