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.

One thing worth noting here is that logic at the end of BIP 341 doesn't
match bitcoind as implemented. The bitcoind logic fixes an off-by-one
error in the BIP itself. In addition, the bitcoind state machine has
further modifications that disallow a transition from started to
defined, which means that at least an activation window must pass before
something can activate.

In practice, this implementation divergence doesn't matter, assuming the
lack of a massive deep re-org (the same assumption bitcoind makes with
the hard coded activation heights). We'll need to wrangle with this
whenever the next soft fork happens.
  • Loading branch information
Roasbeef committed Jan 14, 2022
1 parent 4ec75d4 commit c1842e2
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 5 deletions.
31 changes: 28 additions & 3 deletions blockchain/thresholdstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ type thresholdConditionChecker interface {
// state retarget window.
MinerConfirmationWindow() uint32

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

// 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
Expand Down Expand Up @@ -215,6 +223,10 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit
break
}

// TODO(roasbeef): bitcoind actually never fails here?
// and always goes to started, meaning can activate
// even if deployment has fully expired?

// 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).
Expand Down Expand Up @@ -256,9 +268,22 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit
}

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
41 changes: 41 additions & 0 deletions blockchain/versionbits.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ 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
}

// 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 +174,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 +194,27 @@ 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
}

// Condition returns true when the specific bit defined by the deployment
// associated with the checker is set.
//
Expand Down
35 changes: 33 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,33 @@ 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++ {
_, 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 +329,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 +360,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 c1842e2

Please sign in to comment.