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:
Olaoluwa Osuntokun 2022-01-23 19:06:21 -08:00
parent c6b66ee79c
commit 54f6fa948e
No known key found for this signature in database
GPG Key ID: 3BBD59E99B280306
2 changed files with 285 additions and 81 deletions

View File

@ -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

View File

@ -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)
}
}
}