From a8735edcff4113313dce6aae5352da34d989fdb5 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 19 Mar 2021 17:18:57 -0700 Subject: [PATCH] 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. --- blockchain/chain.go | 16 +++++++++- blockchain/thresholdstate.go | 51 +++++++++++++++++++----------- blockchain/versionbits.go | 61 ++++++++++++++++++++++-------------- 3 files changed, 86 insertions(+), 42 deletions(-) diff --git a/blockchain/chain.go b/blockchain/chain.go index 92bfb26876f..4d1a8394419 100644 --- a/blockchain/chain.go +++ b/blockchain/chain.go @@ -11,12 +11,12 @@ import ( "sync" "time" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/database" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcd/btcutil" ) const ( @@ -1757,6 +1757,20 @@ func New(config *Config) (*BlockChain, error) { 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 // does not yet contain any chain state, both it and the chain state // will be initialized to contain only the genesis block. diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index 5da74a95af6..6f3841bbe1d 100644 --- a/blockchain/thresholdstate.go +++ b/blockchain/thresholdstate.go @@ -6,8 +6,10 @@ package blockchain import ( "fmt" + "time" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" ) // 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 // determine when a consensus rule change threshold should be changed. type thresholdConditionChecker interface { - // BeginTime returns the unix timestamp for the median block time after - // which voting on a rule change starts (at the next window). - BeginTime() uint64 + // HasStarted returns true if based on the passed block blockNode the + // consensus is eligible for deployment. + HasStarted(*blockNode) bool - // EndTime returns the unix timestamp for the median block time after - // which an attempted rule change fails if it has not already been - // locked in or activated. - EndTime() uint64 + // 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 // condition must be true in order to lock in a rule change. @@ -121,6 +122,27 @@ func newThresholdCaches(numCaches uint32) []thresholdStateCache { 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 // AFTER the given node and deployment ID. The cache is used to ensure the // threshold states for previous windows are only calculated once. @@ -150,13 +172,9 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit 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 // been reached yet. - if uint64(medianTime.Unix()) < checker.BeginTime() { + if !checker.HasStarted(prevNode) { cache.Update(&prevNode.hash, ThresholdDefined) break } @@ -192,9 +210,7 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit case ThresholdDefined: // The deployment of the rule change fails if it expires // before it is accepted and locked in. - medianTime := prevNode.CalcPastMedianTime() - medianTimeUnix := uint64(medianTime.Unix()) - if medianTimeUnix >= checker.EndTime() { + if checker.HasEnded(prevNode) { state = ThresholdFailed break } @@ -202,15 +218,14 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit // 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). - if medianTimeUnix >= checker.BeginTime() { + if checker.HasStarted(prevNode) { state = ThresholdStarted } case ThresholdStarted: // The deployment of the rule change fails if it expires // before it is accepted and locked in. - medianTime := prevNode.CalcPastMedianTime() - if uint64(medianTime.Unix()) >= checker.EndTime() { + if checker.HasEnded(prevNode) { state = ThresholdFailed break } diff --git a/blockchain/versionbits.go b/blockchain/versionbits.go index 28fcde7b69a..96c8c3bb2bd 100644 --- a/blockchain/versionbits.go +++ b/blockchain/versionbits.go @@ -5,8 +5,6 @@ package blockchain import ( - "math" - "github.com/btcsuite/btcd/chaincfg" ) @@ -42,27 +40,26 @@ type bitConditionChecker struct { // interface. var _ thresholdConditionChecker = bitConditionChecker{} -// BeginTime returns the unix timestamp for the median block time after which -// voting on a rule change starts (at the next window). +// HasStarted returns true if based on the passed block blockNode the consensus +// 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. // // This is part of the thresholdConditionChecker interface implementation. -func (c bitConditionChecker) BeginTime() uint64 { - return 0 +func (c bitConditionChecker) HasStarted(_ *blockNode) bool { + return true } -// EndTime returns the unix timestamp for the median block time after which an -// attempted rule change fails if it has not already been locked in or -// activated. +// HasStarted returns true if based on the passed block blockNode the consensus +// is eligible for deployment. // -// Since this implementation checks for unknown rules, it returns the maximum -// possible timestamp so the rule is always treated as active. +// Since this implementation checks for unknown rules, it returns false so the +// rule is always treated as active. // // This is part of the thresholdConditionChecker interface implementation. -func (c bitConditionChecker) EndTime() uint64 { - return math.MaxUint64 +func (c bitConditionChecker) HasEnded(_ *blockNode) bool { + return false } // RuleChangeActivationThreshold is the number of blocks for which the condition @@ -123,27 +120,45 @@ type deploymentChecker struct { // interface. var _ thresholdConditionChecker = deploymentChecker{} -// BeginTime returns the unix timestamp for the median block time after which -// voting on a rule change starts (at the next window). +// HasEnded returns true if the target consensus rule change has expired +// or timed out (at the next window). // // This implementation returns the value defined by the specific deployment the // checker is associated with. // // This is part of the thresholdConditionChecker interface implementation. -func (c deploymentChecker) BeginTime() uint64 { - return c.deployment.StartTime +func (c deploymentChecker) HasStarted(blkNode *blockNode) bool { + deploymentStarter := c.deployment.DeploymentStarter + if clockStarter, ok := deploymentStarter.(chaincfg.ClockConsensusDeploymentStarter); ok { + clockStarter.SynchronizeClock(c.chain) + } + + // Can't fail as we make sure to set the clock above. + header := blkNode.Header() + started, _ := deploymentStarter.HasStarted(&header) + + return started } -// EndTime returns the unix timestamp for the median block time after which an -// attempted rule change fails if it has not already been locked in or -// activated. +// HasEnded returns true if the target consensus rule change has expired +// or timed out. // // This implementation returns the value defined by the specific deployment the // checker is associated with. // // This is part of the thresholdConditionChecker interface implementation. -func (c deploymentChecker) EndTime() uint64 { - return c.deployment.ExpireTime +func (c deploymentChecker) HasEnded(blkNode *blockNode) bool { + deploymentEnder := c.deployment.DeploymentEnder + if clockEnder, ok := deploymentEnder.(chaincfg.ClockConsensusDeploymentEnder); ok { + clockEnder.SynchronizeClock(c.chain) + // TODO(roasbeef): do tis upon init of *Blockchain? + } + + // Can't fail as we make sure to set the clock above. + header := blkNode.Header() + ended, _ := deploymentEnder.HasEnded(&header) + + return ended } // RuleChangeActivationThreshold is the number of blocks for which the condition