mirror of
https://github.com/btcsuite/btcd.git
synced 2024-11-19 01:40:07 +01:00
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.
This commit is contained in:
parent
c6b66ee79c
commit
54f6fa948e
@ -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.
|
||||
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user