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"
"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.

View file

@ -14,13 +14,13 @@ import (
"strings"
"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/database/ffldb"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil"
)
const (
@ -357,7 +357,7 @@ func newFakeChain(params *chaincfg.Params) *BlockChain {
targetTimespan := int64(params.TargetTimespan / time.Second)
targetTimePerBlock := int64(params.TargetTimePerBlock / time.Second)
adjustmentFactor := params.RetargetAdjustmentFactor
return &BlockChain{
b := &BlockChain{
chainParams: params,
timeSource: NewMedianTime(),
minRetargetTimespan: targetTimespan / adjustmentFactor,
@ -368,6 +368,20 @@ func newFakeChain(params *chaincfg.Params) *BlockChain {
warningCaches: newThresholdCaches(vbNumBits),
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

View file

@ -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
}

View file

@ -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,36 @@ 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 {
// 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
// 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 {
// 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