blockchain: implement AlwaysActiveHeight for forced deployment activation

This commit introduces the concept of `AlwaysActiveHeight` to the
deployment mechanism, allowing a deployment to be forced into the active
state if the next block's height meets or exceeds this threshold.

This is intended primarily to be used alongside the new Testnet4
deployment, as the past major soft forks are meant to be active from the
very first block height.
This commit is contained in:
Olaoluwa Osuntokun 2025-03-10 16:38:57 -05:00
parent 9429f7def5
commit 987745ed25
7 changed files with 148 additions and 10 deletions

1
.gitignore vendored
View file

@ -57,3 +57,4 @@ btcutil/psbt/coverage.txt
/gencerts
.DS_Store
.aider*

View file

@ -102,6 +102,11 @@ type thresholdConditionChecker interface {
// not the bit associated with the condition is set, but can be more
// complex as needed.
Condition(*blockNode) (bool, error)
// ForceActive returns if the deployment should be forced to transition
// to the active state. This is useful on certain testnet, where we
// we'd like for a deployment to always be active.
ForceActive(*blockNode) bool
}
// thresholdStateCache provides a type to cache the threshold states of each
@ -279,7 +284,17 @@ func thresholdStateTransition(state ThresholdState, prevNode *blockNode,
// threshold states for previous windows are only calculated once.
//
// This function MUST be called with the chain state lock held (for writes).
func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdConditionChecker, cache *thresholdStateCache) (ThresholdState, error) {
func (b *BlockChain) thresholdState(prevNode *blockNode,
checker thresholdConditionChecker,
cache *thresholdStateCache) (ThresholdState, error) {
// If the deployment has a nonzero AlwaysActiveHeight and the next
// blocks height is at or above that threshold, then force the state
// to Active.
if checker.ForceActive(prevNode) {
return ThresholdActive, nil
}
// The threshold state for the window that contains the genesis block is
// defined by definition.
confirmationWindow := int32(checker.MinerConfirmationWindow())

View file

@ -175,6 +175,10 @@ func (c customDeploymentChecker) Condition(_ *blockNode) (bool, error) {
return c.conditionTrue, nil
}
func (c customDeploymentChecker) ForceActive(_ *blockNode) bool {
return false
}
// TestThresholdStateTransition tests that the thresholdStateTransition
// properly implements the BIP 009 state machine, along with the speedy trial
// augments.

View file

@ -134,6 +134,13 @@ func (c bitConditionChecker) IsSpeedy() bool {
return false
}
// ForceActive returns if the deployment should be forced to transition to the
// active state. This is useful on certain testnet, where we we'd like for a
// deployment to always be active.
func (c bitConditionChecker) ForceActive(node *blockNode) 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.
@ -207,15 +214,9 @@ func (c deploymentChecker) 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.
//
// This implementation always returns true, unless a minimum activation height
// is specified.
//
// This is part of the thresholdConditionChecker interface implementation.
// the LockedIn to the Active state. In addition to the traditional minimum
// activation height (MinActivationHeight), an optional AlwaysActiveHeight can
// force the deployment to be active after a specified height.
func (c deploymentChecker) EligibleToActivate(blkNode *blockNode) bool {
// No activation height, so it's always ready to go.
if c.deployment.MinActivationHeight == 0 {
@ -249,6 +250,28 @@ func (c deploymentChecker) Condition(node *blockNode) (bool, error) {
nil
}
// ForceActive returns if the deployment should be forced to transition to the
// active state. This is useful on certain testnet, where we we'd like for a
// deployment to always be active.
func (c deploymentChecker) ForceActive(node *blockNode) bool {
if node == nil {
return false
}
// If the deployment has a nonzero AlwaysActiveHeight and the next
// blocks height is at or above that threshold, then force the state
// to Active.
effectiveHeight := c.deployment.EffectiveAlwaysActiveHeight()
if uint32(node.height)+1 >= effectiveHeight {
log.Debugf("Force activating deployment: next block "+
"height %d >= EffectiveAlwaysActiveHeight %d",
uint32(node.height)+1, effectiveHeight)
return true
}
return false
}
// calcNextBlockVersion calculates the expected version of the block after the
// passed previous block node based on the state of started and locked in
// rule change deployments.

View file

@ -8,6 +8,7 @@ import (
"encoding/binary"
"encoding/hex"
"errors"
"math"
"math/big"
"strings"
"time"
@ -78,6 +79,16 @@ type Checkpoint struct {
Hash *chainhash.Hash
}
// EffectiveAlwaysActiveHeight returns the effective activation height for the
// deployment. If AlwaysActiveHeight is unset (i.e. zero), it returns
// the maximum uint32 value to indicate that it does not force activation.
func (d *ConsensusDeployment) EffectiveAlwaysActiveHeight() uint32 {
if d.AlwaysActiveHeight == 0 {
return math.MaxUint32
}
return d.AlwaysActiveHeight
}
// DNSSeed identifies a DNS seed.
type DNSSeed struct {
// Host defines the hostname of the seed.
@ -108,6 +119,11 @@ type ConsensusDeployment struct {
// activation. A value of 1815 block denotes a 90% threshold.
CustomActivationThreshold uint32
// AlwaysActiveHeight defines an optional block threshold at which the
// deployment is forced to be active. If unset (0), it defaults to
// math.MaxUint32, meaning the deployment does not force activation.
AlwaysActiveHeight uint32
// DeploymentStarter is used to determine if the given
// ConsensusDeployment has started or not.
DeploymentStarter ConsensusDeploymentStarter
@ -146,6 +162,10 @@ const (
// the deployment of BIPS 340, 341 and 342.
DeploymentTaproot
// DeploymentTestDummyAlwaysActive is a dummy deployment that is meant
// to always be active.
DeploymentTestDummyAlwaysActive
// NOTE: DefinedDeployments must always come last since it is used to
// determine how many defined deployments there currently are.
@ -379,6 +399,16 @@ var MainNetParams = Params{
time.Time{}, // Never expires
),
},
DeploymentTestDummyAlwaysActive: {
BitNumber: 30,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
AlwaysActiveHeight: 1,
},
DeploymentCSV: {
BitNumber: 0,
DeploymentStarter: NewMedianTimeDeploymentStarter(
@ -490,6 +520,16 @@ var RegressionNetParams = Params{
time.Time{}, // Never expires
),
},
DeploymentTestDummyAlwaysActive: {
BitNumber: 30,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
AlwaysActiveHeight: 1,
},
DeploymentCSV: {
BitNumber: 0,
DeploymentStarter: NewMedianTimeDeploymentStarter(
@ -624,6 +664,16 @@ var TestNet3Params = Params{
time.Time{}, // Never expires
),
},
DeploymentTestDummyAlwaysActive: {
BitNumber: 30,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
AlwaysActiveHeight: 1,
},
DeploymentCSV: {
BitNumber: 0,
DeploymentStarter: NewMedianTimeDeploymentStarter(
@ -736,6 +786,16 @@ var TestNet4Params = Params{
time.Time{}, // Never expires
),
},
DeploymentTestDummyAlwaysActive: {
BitNumber: 30,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
AlwaysActiveHeight: 1,
},
DeploymentCSV: {
BitNumber: 29,
DeploymentStarter: NewMedianTimeDeploymentStarter(
@ -744,6 +804,7 @@ var TestNet4Params = Params{
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
AlwaysActiveHeight: 1,
},
DeploymentSegwit: {
BitNumber: 29,
@ -753,6 +814,7 @@ var TestNet4Params = Params{
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
AlwaysActiveHeight: 1,
},
DeploymentTaproot: {
BitNumber: 2,
@ -763,6 +825,7 @@ var TestNet4Params = Params{
time.Time{}, // Never expires
),
MinActivationHeight: 0,
AlwaysActiveHeight: 1,
},
},
@ -877,6 +940,16 @@ var SimNetParams = Params{
),
CustomActivationThreshold: 75, // Only needs 75% hash rate.
},
DeploymentTestDummyAlwaysActive: {
BitNumber: 29,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
AlwaysActiveHeight: 1,
},
},
// Mempool parameters
@ -977,6 +1050,16 @@ func CustomSignetParams(challenge []byte, dnsSeeds []DNSSeed) Params {
time.Time{}, // Never expires
),
},
DeploymentTestDummyAlwaysActive: {
BitNumber: 30,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
AlwaysActiveHeight: 1,
},
DeploymentCSV: {
BitNumber: 29,
DeploymentStarter: NewMedianTimeDeploymentStarter(

View file

@ -139,6 +139,14 @@ func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) {
}
defer r.TearDown()
// If the deployment is meant to be always active, then it should be
// active from the very first block.
if deploymentID == chaincfg.DeploymentTestDummyAlwaysActive {
assertChainHeight(r, t, 0)
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdActive)
return
}
// *** ThresholdDefined ***
//
// Assert the chain height is the expected value and the soft fork
@ -340,6 +348,7 @@ func TestBIP0009(t *testing.T) {
testBIP0009(t, "dummy", chaincfg.DeploymentTestDummy)
testBIP0009(t, "dummy-min-activation", chaincfg.DeploymentTestDummyMinActivation)
testBIP0009(t, "dummy-always-active", chaincfg.DeploymentTestDummyAlwaysActive)
testBIP0009(t, "segwit", chaincfg.DeploymentSegwit)
}

View file

@ -1258,6 +1258,9 @@ func handleGetBlockChainInfo(s *rpcServer, cmd interface{}, closeChan <-chan str
case chaincfg.DeploymentTestDummyMinActivation:
forkName = "dummy-min-activation"
case chaincfg.DeploymentTestDummyAlwaysActive:
forkName = "dummy-always-active"
case chaincfg.DeploymentCSV:
forkName = "csv"