Skip to content

Commit

Permalink
blockchain: update version bits logic to use HasStarted/HasEnded for …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
Roasbeef committed Jan 12, 2022
1 parent 9f6f89d commit a8735ed
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 42 deletions.
16 changes: 15 additions & 1 deletion blockchain/chain.go
Expand Up @@ -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 (
Expand Down Expand Up @@ -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.
Expand Down
51 changes: 33 additions & 18 deletions blockchain/thresholdstate.go
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -192,25 +210,22 @@ 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
}

// 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
}
Expand Down
61 changes: 38 additions & 23 deletions blockchain/versionbits.go
Expand Up @@ -5,8 +5,6 @@
package blockchain

import (
"math"

"github.com/btcsuite/btcd/chaincfg"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit a8735ed

Please sign in to comment.