diff --git a/.gitignore b/.gitignore index 3b742cea..acfb8c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ btcutil/psbt/coverage.txt /gencerts .DS_Store +.aider* diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index d62c2de3..88031019 100644 --- a/blockchain/thresholdstate.go +++ b/blockchain/thresholdstate.go @@ -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 + // block’s 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()) diff --git a/blockchain/thresholdstate_test.go b/blockchain/thresholdstate_test.go index 8d527137..28f417a1 100644 --- a/blockchain/thresholdstate_test.go +++ b/blockchain/thresholdstate_test.go @@ -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. diff --git a/blockchain/versionbits.go b/blockchain/versionbits.go index 371d4f20..493787a7 100644 --- a/blockchain/versionbits.go +++ b/blockchain/versionbits.go @@ -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 + // block’s 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. diff --git a/chaincfg/params.go b/chaincfg/params.go index d97353f5..eb4f062e 100644 --- a/chaincfg/params.go +++ b/chaincfg/params.go @@ -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( diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index 5b644804..8f8b59a5 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -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) } diff --git a/rpcserver.go b/rpcserver.go index 7483011c..736f1459 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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"