blockchain: update version bits logic to use HasStarted/HasEnded for deployments

In this commit, we update our version bits logic to use the newly added
HasStarted and HasEnded methods for consensus deployments. Along the
way, wee modify the thresholdConditionChecker` interface to be based off
the new chaincfg interfaces. In addition, we add a new method
`PastMedianTime`, in order to allow the chain itself to be used as a
`chaincfg.BlockClock`.

This serves to make the logic more generic in order to support both
block height and time based soft fork timeouts.
This commit is contained in:
Olaoluwa Osuntokun 2021-03-19 17:18:57 -07:00
parent 2b6370dfd7
commit 298d6165be
No known key found for this signature in database
GPG key ID: 3BBD59E99B280306
4 changed files with 93 additions and 44 deletions

View file

@ -11,12 +11,12 @@ import (
"sync" "sync"
"time" "time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/database"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil"
) )
const ( const (
@ -1757,6 +1757,20 @@ func New(config *Config) (*BlockChain, error) {
deploymentCaches: newThresholdCaches(chaincfg.DefinedDeployments), deploymentCaches: newThresholdCaches(chaincfg.DefinedDeployments),
} }
// Ensure all the deployments are synchronized with our clock if
// needed.
for _, deployment := range b.chainParams.Deployments {
deploymentStarter := deployment.DeploymentStarter
if clockStarter, ok := deploymentStarter.(chaincfg.ClockConsensusDeploymentStarter); ok {
clockStarter.SynchronizeClock(&b)
}
deploymentEnder := deployment.DeploymentEnder
if clockEnder, ok := deploymentEnder.(chaincfg.ClockConsensusDeploymentEnder); ok {
clockEnder.SynchronizeClock(&b)
}
}
// Initialize the chain state from the passed database. When the db // Initialize the chain state from the passed database. When the db
// does not yet contain any chain state, both it and the chain state // does not yet contain any chain state, both it and the chain state
// will be initialized to contain only the genesis block. // will be initialized to contain only the genesis block.

View file

@ -14,13 +14,13 @@ import (
"strings" "strings"
"time" "time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/database"
_ "github.com/btcsuite/btcd/database/ffldb" _ "github.com/btcsuite/btcd/database/ffldb"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil"
) )
const ( const (
@ -357,7 +357,7 @@ func newFakeChain(params *chaincfg.Params) *BlockChain {
targetTimespan := int64(params.TargetTimespan / time.Second) targetTimespan := int64(params.TargetTimespan / time.Second)
targetTimePerBlock := int64(params.TargetTimePerBlock / time.Second) targetTimePerBlock := int64(params.TargetTimePerBlock / time.Second)
adjustmentFactor := params.RetargetAdjustmentFactor adjustmentFactor := params.RetargetAdjustmentFactor
return &BlockChain{ b := &BlockChain{
chainParams: params, chainParams: params,
timeSource: NewMedianTime(), timeSource: NewMedianTime(),
minRetargetTimespan: targetTimespan / adjustmentFactor, minRetargetTimespan: targetTimespan / adjustmentFactor,
@ -368,6 +368,20 @@ func newFakeChain(params *chaincfg.Params) *BlockChain {
warningCaches: newThresholdCaches(vbNumBits), warningCaches: newThresholdCaches(vbNumBits),
deploymentCaches: newThresholdCaches(chaincfg.DefinedDeployments), deploymentCaches: newThresholdCaches(chaincfg.DefinedDeployments),
} }
for _, deployment := range params.Deployments {
deploymentStarter := deployment.DeploymentStarter
if clockStarter, ok := deploymentStarter.(chaincfg.ClockConsensusDeploymentStarter); ok {
clockStarter.SynchronizeClock(b)
}
deploymentEnder := deployment.DeploymentEnder
if clockEnder, ok := deploymentEnder.(chaincfg.ClockConsensusDeploymentEnder); ok {
clockEnder.SynchronizeClock(b)
}
}
return b
} }
// newFakeNode creates a block node connected to the passed parent with the // newFakeNode creates a block node connected to the passed parent with the

View file

@ -6,8 +6,10 @@ package blockchain
import ( import (
"fmt" "fmt"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
) )
// ThresholdState define the various threshold states used when voting on // ThresholdState define the various threshold states used when voting on
@ -66,14 +68,13 @@ func (t ThresholdState) String() string {
// thresholdConditionChecker provides a generic interface that is invoked to // thresholdConditionChecker provides a generic interface that is invoked to
// determine when a consensus rule change threshold should be changed. // determine when a consensus rule change threshold should be changed.
type thresholdConditionChecker interface { type thresholdConditionChecker interface {
// BeginTime returns the unix timestamp for the median block time after // HasStarted returns true if based on the passed block blockNode the
// which voting on a rule change starts (at the next window). // consensus is eligible for deployment.
BeginTime() uint64 HasStarted(*blockNode) bool
// EndTime returns the unix timestamp for the median block time after // HasEnded returns true if the target consensus rule change has expired
// which an attempted rule change fails if it has not already been // or timed out.
// locked in or activated. HasEnded(*blockNode) bool
EndTime() uint64
// RuleChangeActivationThreshold is the number of blocks for which the // RuleChangeActivationThreshold is the number of blocks for which the
// condition must be true in order to lock in a rule change. // condition must be true in order to lock in a rule change.
@ -121,6 +122,27 @@ func newThresholdCaches(numCaches uint32) []thresholdStateCache {
return caches return caches
} }
// PastMedianTime returns the past median time from the PoV of the passed block
// header. The past median time is the median time of the 11 blocks prior to
// the passed block header.
//
// NOTE: This is part of the chainfg.BlockClock interface
func (b *BlockChain) PastMedianTime(blockHeader *wire.BlockHeader) (time.Time, error) {
prevHash := blockHeader.PrevBlock
prevNode := b.index.LookupNode(&prevHash)
// If we can't find the previous node, then we can't compute the block
// time since it requires us to walk backwards from this node.
if prevNode == nil {
return time.Time{}, fmt.Errorf("blockHeader(%v) has no "+
"previous node", blockHeader.BlockHash())
}
blockNode := newBlockNode(blockHeader, prevNode)
return blockNode.CalcPastMedianTime(), nil
}
// thresholdState returns the current rule change threshold state for the block // 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 // AFTER the given node and deployment ID. The cache is used to ensure the
// threshold states for previous windows are only calculated once. // threshold states for previous windows are only calculated once.
@ -150,13 +172,9 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit
break break
} }
// The start and expiration times are based on the median block
// time, so calculate it now.
medianTime := prevNode.CalcPastMedianTime()
// The state is simply defined if the start time hasn't been // The state is simply defined if the start time hasn't been
// been reached yet. // been reached yet.
if uint64(medianTime.Unix()) < checker.BeginTime() { if !checker.HasStarted(prevNode) {
cache.Update(&prevNode.hash, ThresholdDefined) cache.Update(&prevNode.hash, ThresholdDefined)
break break
} }
@ -192,9 +210,7 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit
case ThresholdDefined: case ThresholdDefined:
// The deployment of the rule change fails if it expires // The deployment of the rule change fails if it expires
// before it is accepted and locked in. // before it is accepted and locked in.
medianTime := prevNode.CalcPastMedianTime() if checker.HasEnded(prevNode) {
medianTimeUnix := uint64(medianTime.Unix())
if medianTimeUnix >= checker.EndTime() {
state = ThresholdFailed state = ThresholdFailed
break break
} }
@ -202,15 +218,14 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit
// The state for the rule moves to the started state // The state for the rule moves to the started state
// once its start time has been reached (and it hasn't // once its start time has been reached (and it hasn't
// already expired per the above). // already expired per the above).
if medianTimeUnix >= checker.BeginTime() { if checker.HasStarted(prevNode) {
state = ThresholdStarted state = ThresholdStarted
} }
case ThresholdStarted: case ThresholdStarted:
// The deployment of the rule change fails if it expires // The deployment of the rule change fails if it expires
// before it is accepted and locked in. // before it is accepted and locked in.
medianTime := prevNode.CalcPastMedianTime() if checker.HasEnded(prevNode) {
if uint64(medianTime.Unix()) >= checker.EndTime() {
state = ThresholdFailed state = ThresholdFailed
break break
} }

View file

@ -5,8 +5,6 @@
package blockchain package blockchain
import ( import (
"math"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
) )
@ -42,27 +40,26 @@ type bitConditionChecker struct {
// interface. // interface.
var _ thresholdConditionChecker = bitConditionChecker{} var _ thresholdConditionChecker = bitConditionChecker{}
// BeginTime returns the unix timestamp for the median block time after which // HasStarted returns true if based on the passed block blockNode the consensus
// voting on a rule change starts (at the next window). // is eligible for deployment.
// //
// Since this implementation checks for unknown rules, it returns 0 so the rule // Since this implementation checks for unknown rules, it returns true so
// is always treated as active. // is always treated as active.
// //
// This is part of the thresholdConditionChecker interface implementation. // This is part of the thresholdConditionChecker interface implementation.
func (c bitConditionChecker) BeginTime() uint64 { func (c bitConditionChecker) HasStarted(_ *blockNode) bool {
return 0 return true
} }
// EndTime returns the unix timestamp for the median block time after which an // HasStarted returns true if based on the passed block blockNode the consensus
// attempted rule change fails if it has not already been locked in or // is eligible for deployment.
// activated.
// //
// Since this implementation checks for unknown rules, it returns the maximum // Since this implementation checks for unknown rules, it returns false so the
// possible timestamp so the rule is always treated as active. // rule is always treated as active.
// //
// This is part of the thresholdConditionChecker interface implementation. // This is part of the thresholdConditionChecker interface implementation.
func (c bitConditionChecker) EndTime() uint64 { func (c bitConditionChecker) HasEnded(_ *blockNode) bool {
return math.MaxUint64 return false
} }
// RuleChangeActivationThreshold is the number of blocks for which the condition // RuleChangeActivationThreshold is the number of blocks for which the condition
@ -123,27 +120,36 @@ type deploymentChecker struct {
// interface. // interface.
var _ thresholdConditionChecker = deploymentChecker{} var _ thresholdConditionChecker = deploymentChecker{}
// BeginTime returns the unix timestamp for the median block time after which // HasEnded returns true if the target consensus rule change has expired
// voting on a rule change starts (at the next window). // or timed out (at the next window).
// //
// This implementation returns the value defined by the specific deployment the // This implementation returns the value defined by the specific deployment the
// checker is associated with. // checker is associated with.
// //
// This is part of the thresholdConditionChecker interface implementation. // This is part of the thresholdConditionChecker interface implementation.
func (c deploymentChecker) BeginTime() uint64 { func (c deploymentChecker) HasStarted(blkNode *blockNode) bool {
return c.deployment.StartTime // Can't fail as we make sure to set the clock above when we
// instantiate *BlockChain.
header := blkNode.Header()
started, _ := c.deployment.DeploymentStarter.HasStarted(&header)
return started
} }
// EndTime returns the unix timestamp for the median block time after which an // HasEnded returns true if the target consensus rule change has expired
// attempted rule change fails if it has not already been locked in or // or timed out.
// activated.
// //
// This implementation returns the value defined by the specific deployment the // This implementation returns the value defined by the specific deployment the
// checker is associated with. // checker is associated with.
// //
// This is part of the thresholdConditionChecker interface implementation. // This is part of the thresholdConditionChecker interface implementation.
func (c deploymentChecker) EndTime() uint64 { func (c deploymentChecker) HasEnded(blkNode *blockNode) bool {
return c.deployment.ExpireTime // Can't fail as we make sure to set the clock above when we
// instantiate *BlockChain.
header := blkNode.Header()
ended, _ := c.deployment.DeploymentEnder.HasEnded(&header)
return ended
} }
// RuleChangeActivationThreshold is the number of blocks for which the condition // RuleChangeActivationThreshold is the number of blocks for which the condition