Skip to content

Commit

Permalink
blockchain+integration: add support for min activation height and cus…
Browse files Browse the repository at this point in the history
…tom thresholds

In this commit, we extend the existing version bits state machine to add
support for the new minimum activation height and custom block threshold
for activation. We then extend the existing BIP 9 tests (tho this isn't
really BIP 9 anymore...) to exercise the new min activation height
logic.
  • Loading branch information
Roasbeef committed Jan 25, 2022
1 parent 38737a8 commit c6b66ee
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 18 deletions.
68 changes: 52 additions & 16 deletions blockchain/thresholdstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ type thresholdConditionChecker interface {
// consensus is eligible for deployment.
HasStarted(*blockNode) bool

// HasEnded returns true if the target consensus rule change has expired
// or timed out.
// HasEnded returns true if the target consensus rule change has
// expired or timed out.
HasEnded(*blockNode) bool

// RuleChangeActivationThreshold is the number of blocks for which the
Expand All @@ -84,10 +84,23 @@ type thresholdConditionChecker interface {
// state retarget window.
MinerConfirmationWindow() uint32

// Condition returns whether or not the rule change activation condition
// has been met. This typically involves checking whether or not the
// bit associated with the condition is set, but can be more complex as
// needed.
// EligibleToActivate returns true if a custom deployment can
// transition from the LockedIn to the Active state. For normal
// deployments, this always returns true. However, some deployments add
// extra rules like a minimum activation height, which can be
// abstracted into a generic arbitrary check at the final state via
// this method.
EligibleToActivate(*blockNode) bool

// IsSpeedy returns true if this is to be a "speedy" deployment. A
// speedy deployment differs from a regular one in that only after a
// miner block confirmation window can the deployment expire.
IsSpeedy() bool

// Condition returns whether or not the rule change activation
// condition has been met. This typically involves checking whether or
// not the bit associated with the condition is set, but can be more
// complex as needed.
Condition(*blockNode) (bool, error)
}

Expand Down Expand Up @@ -208,9 +221,11 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit

switch state {
case ThresholdDefined:
// The deployment of the rule change fails if it expires
// before it is accepted and locked in.
if checker.HasEnded(prevNode) {
// 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
}
Expand All @@ -223,9 +238,10 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit
}

case ThresholdStarted:
// The deployment of the rule change fails if it expires
// before it is accepted and locked in.
if checker.HasEnded(prevNode) {
// 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
}
Expand All @@ -248,17 +264,37 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit
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.
if count >= checker.RuleChangeActivationThreshold() {
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:
// The new rule becomes active when its previous state
// was locked in.
state = ThresholdActive
// 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.
Expand Down
63 changes: 63 additions & 0 deletions blockchain/versionbits.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,32 @@ func (c bitConditionChecker) Condition(node *blockNode) (bool, error) {
return uint32(expectedVersion)&conditionMask == 0, nil
}

// EligibleToActivate returns true if a custom deployment can transition from
// the LockedIn to the Active state. For normal deployments, this always
// returns true. However, some deployments add extra rules like a minimum
// activation height, which can be abstracted into a generic arbitrary check at
// the final state via this method.
//
// This implementation always returns true, as it's used to warn about other
// unknown deployments.
//
// This is part of the thresholdConditionChecker interface implementation.
func (c bitConditionChecker) EligibleToActivate(blkNode *blockNode) bool {
return true
}

// IsSpeedy returns true if this is to be a "speedy" deployment. A speedy
// deployment differs from a regular one in that only after a miner block
// confirmation window can the deployment expire.
//
// This implementation returns false, as we want to always be warned if
// something is about to activate.
//
// This is part of the thresholdConditionChecker interface implementation.
func (c bitConditionChecker) IsSpeedy() bool {
return false
}

// deploymentChecker provides a thresholdConditionChecker which can be used to
// test a specific deployment rule. This is required for properly detecting
// and activating consensus rule changes.
Expand Down Expand Up @@ -160,6 +186,12 @@ func (c deploymentChecker) HasEnded(blkNode *blockNode) bool {
//
// This is part of the thresholdConditionChecker interface implementation.
func (c deploymentChecker) RuleChangeActivationThreshold() uint32 {
// Some deployments like taproot used a custom activation threshold
// that ovverides the network level threshold.
if c.deployment.CustomActivationThreshold != 0 {
return c.deployment.CustomActivationThreshold
}

return c.chain.chainParams.RuleChangeActivationThreshold
}

Expand All @@ -174,6 +206,37 @@ func (c deploymentChecker) MinerConfirmationWindow() uint32 {
return c.chain.chainParams.MinerConfirmationWindow
}

// EligibleToActivate returns true if a custom deployment can transition from
// the LockedIn to the Active state. For normal deployments, this always
// returns true. However, some deployments add extra rules like a minimum
// activation height, which can be abstracted into a generic arbitrary check at
// the final state via this method.
//
// This implementation always returns true, unless a minimum activation height
// is specified.
//
// This is part of the thresholdConditionChecker interface implementation.
func (c deploymentChecker) EligibleToActivate(blkNode *blockNode) bool {
// No activation height, so it's always ready to go.
if c.deployment.MinActivationHeight == 0 {
return true
}

// If the _next_ block (as this is the prior block to the one being
// connected is the min height or beyond, then this can activate.
return uint32(blkNode.height)+1 >= c.deployment.MinActivationHeight
}

// IsSpeedy returns true if this is to be a "speedy" deployment. A speedy
// deployment differs from a regular one in that only after a miner block
// confirmation window can the deployment expire. This implementation returns
// true if a min activation height is set.
//
// This is part of the thresholdConditionChecker interface implementation.
func (c deploymentChecker) IsSpeedy() bool {
return c.deployment.MinActivationHeight != 0
}

// Condition returns true when the specific bit defined by the deployment
// associated with the checker is set.
//
Expand Down
44 changes: 42 additions & 2 deletions integration/bip0009_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.

// This file is ignored during the regular tests due to the following build tag.
//go:build rpctest
// +build rpctest

package integration
Expand Down Expand Up @@ -196,6 +197,9 @@ func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) {
}
deployment := &r.ActiveNet.Deployments[deploymentID]
activationThreshold := r.ActiveNet.RuleChangeActivationThreshold
if deployment.CustomActivationThreshold != 0 {
activationThreshold = deployment.CustomActivationThreshold
}
signalForkVersion := int32(1<<deployment.BitNumber) | vbTopBits
for i := uint32(0); i < activationThreshold-1; i++ {
_, err := r.GenerateAndSubmitBlock(nil, signalForkVersion,
Expand Down Expand Up @@ -268,7 +272,42 @@ func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) {
if err != nil {
t.Fatalf("failed to generated block: %v", err)
}
assertChainHeight(r, t, (confirmationWindow*4)-1)
expectedChainHeight := (confirmationWindow * 4) - 1
assertChainHeight(r, t, expectedChainHeight)

// If this isn't a fork that has a min activation height set, then it
// should be active at this point.
if deployment.MinActivationHeight == 0 {
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdActive)
return
}

// Otherwise, we'll need to mine additional blocks to pass the min
// activation height and ensure the rule set applies. For regtest the
// deployment can only activate after height 600, and at this point
// we've mined 4*144 blocks, so another confirmation window will put us
// over.
numBlocksLeft := confirmationWindow
for i := uint32(0); i < numBlocksLeft; i++ {
// Ensure that we're always in the locked in state right up
// until after we mine the very last block.
if i < numBlocksLeft {
assertSoftForkStatus(
r, t, forkKey, blockchain.ThresholdLockedIn,
)
}

_, err := r.GenerateAndSubmitBlock(
nil, signalForkVersion, time.Time{},
)
if err != nil {
t.Fatalf("failed to generated block %d: %v", i, err)
}
}

// At this point, the soft fork should now be shown as active.
expectedChainHeight = (confirmationWindow * 5) - 1
assertChainHeight(r, t, expectedChainHeight)
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdActive)
}

Expand Down Expand Up @@ -299,6 +338,7 @@ func TestBIP0009(t *testing.T) {
t.Parallel()

testBIP0009(t, "dummy", chaincfg.DeploymentTestDummy)
testBIP0009(t, "dummy-min-activation", chaincfg.DeploymentTestDummyMinActivation)
testBIP0009(t, "segwit", chaincfg.DeploymentSegwit)
}

Expand Down Expand Up @@ -329,7 +369,7 @@ func TestBIP0009Mining(t *testing.T) {
}
defer r.TearDown()

// Assert the chain only consists of the gensis block.
// Assert the chain only consists of the genesis block.
assertChainHeight(r, t, 0)

// *** ThresholdDefined ***
Expand Down

0 comments on commit c6b66ee

Please sign in to comment.