From 54f6fa948e6e73d13f2761f118aa20b6871acfee Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Sun, 23 Jan 2022 19:06:21 -0800 Subject: [PATCH] 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. --- blockchain/thresholdstate.go | 182 ++++++++++++++++------------- blockchain/thresholdstate_test.go | 184 ++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+), 81 deletions(-) diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index 29b62e468f..b96c9bd3db 100644 --- a/blockchain/thresholdstate.go +++ b/blockchain/thresholdstate.go @@ -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 diff --git a/blockchain/thresholdstate_test.go b/blockchain/thresholdstate_test.go index c65f5a4465..8d527137e3 100644 --- a/blockchain/thresholdstate_test.go +++ b/blockchain/thresholdstate_test.go @@ -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) + } + } +}