Skip to content

Commit

Permalink
Merge pull request #1700 from Roasbeef/bip-8-prep
Browse files Browse the repository at this point in the history
chaincfg+blockchain: abstract/refactor BIP 9 version bits implementation to work w/ BIP 8 block heights
  • Loading branch information
Roasbeef committed Jan 26, 2022
2 parents c5e6d3c + 0b245cc commit 588c071
Show file tree
Hide file tree
Showing 11 changed files with 897 additions and 187 deletions.
9 changes: 5 additions & 4 deletions Makefile
Expand Up @@ -15,7 +15,8 @@ DEPGET := cd /tmp && GO111MODULE=on go get -v
GOBUILD := GO111MODULE=on go build -v
GOINSTALL := GO111MODULE=on go install -v
DEV_TAGS := rpctest
GOTEST := GO111MODULE=on go test -v -tags=$(DEV_TAGS)
GOTEST_DEV = GO111MODULE=on go test -v -tags=$(DEV_TAGS)
GOTEST := GO111MODULE=on go test -v

GOFILES_NOVENDOR = $(shell find . -type f -name '*.go' -not -path "./vendor/*")

Expand Down Expand Up @@ -78,9 +79,9 @@ check: unit

unit:
@$(call print, "Running unit tests.")
$(GOTEST) ./... -test.timeout=20m
cd btcutil; $(GOTEST) ./... -test.timeout=20m
cd btcutil/psbt; $(GOTEST) ./... -test.timeout=20m
$(GOTEST_DEV) ./... -test.timeout=20m
cd btcutil; $(GOTEST_DEV) ./... -test.timeout=20m
cd btcutil/psbt; $(GOTEST_DEV) ./... -test.timeout=20m

unit-cover: $(GOACC_BIN)
@$(call print, "Running unit coverage tests.")
Expand Down
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
18 changes: 16 additions & 2 deletions blockchain/common_test.go
Expand Up @@ -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 (
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
225 changes: 148 additions & 77 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 All @@ -83,10 +84,23 @@ type thresholdConditionChecker interface {
// state retarget window.
MinerConfirmationWindow() uint32

// Condition returns whether or not the rule change activation condition
// has been met. This typically involves checking whether or not the
// bit associated with the condition is set, but can be more complex as
// needed.
// 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.
EligibleToActivate(*blockNode) bool

// IsSpeedy returns true if this is to be a "speedy" deployment. A
// speedy deployment differs from a regular one in that only after a
// miner block confirmation window can the deployment expire.
IsSpeedy() bool

// Condition returns whether or not the rule change activation
// condition has been met. This typically involves checking whether or
// not the bit associated with the condition is set, but can be more
// complex as needed.
Condition(*blockNode) (bool, error)
}

Expand Down Expand Up @@ -121,6 +135,120 @@ 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
}

// thresholdStateTransition given a state, a previous node, and a toeholds
// checker, this function transitions to the next state as defined by BIP 009.
// This state transition function is also aware of the "speedy trial"
// modifications made to BIP 0009 as part of the taproot softfork activation.
func thresholdStateTransition(state ThresholdState, prevNode *blockNode,
checker thresholdConditionChecker,
confirmationWindow int32) (ThresholdState, error) {

switch state {
case ThresholdDefined:
// The deployment of the rule change fails if it
// expires before it is accepted and locked in. However
// speed deployments can only transition to failed
// after a confirmation window.
if !checker.IsSpeedy() && 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 checker.HasStarted(prevNode) {
state = ThresholdStarted
}

case ThresholdStarted:
// The deployment of the rule change fails if it
// expires before it is accepted and locked in, but
// only if this deployment isn't speedy.
if !checker.IsSpeedy() && checker.HasEnded(prevNode) {
state = ThresholdFailed
break
}

// At this point, the rule change is still being voted
// on by the miners, so iterate backwards through the
// confirmation window to count all of the votes in it.
var count uint32
countNode := prevNode
for i := int32(0); i < confirmationWindow; i++ {
condition, err := checker.Condition(countNode)
if err != nil {
return ThresholdFailed, err
}
if condition {
count++
}

// Get the previous block node.
countNode = countNode.parent
}

switch {
// The state is locked in if the number of blocks in the
// period that voted for the rule change meets the
// activation threshold.
case count >= checker.RuleChangeActivationThreshold():
state = ThresholdLockedIn

// If this is a speedy deployment, we didn't meet the
// threshold above, and the deployment has expired, then
// we transition to failed.
case checker.IsSpeedy() && checker.HasEnded(prevNode):
state = ThresholdFailed
}

case ThresholdLockedIn:
// At this point, we'll consult the deployment see if a
// custom deployment has any other arbitrary conditions
// that need to pass before execution. This might be a
// minimum activation height or another policy.
//
// If we aren't eligible to active yet, then we'll just
// stay in the locked in position.
if !checker.EligibleToActivate(prevNode) {
state = ThresholdLockedIn
} else {
// The new rule becomes active when its
// previous state was locked in assuming it's
// now eligible to activate.
state = ThresholdActive
}

// Nothing to do if the previous state is active or failed since
// they are both terminal states.
case ThresholdActive:
case ThresholdFailed:
}

return state, 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 +278,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 @@ -185,70 +309,17 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit

// Since each threshold state depends on the state of the previous
// window, iterate starting from the oldest unknown window.
var err error
for neededNum := len(neededStates) - 1; neededNum >= 0; neededNum-- {
prevNode := neededStates[neededNum]

switch state {
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() {
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() {
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() {
state = ThresholdFailed
break
}

// At this point, the rule change is still being voted
// on by the miners, so iterate backwards through the
// confirmation window to count all of the votes in it.
var count uint32
countNode := prevNode
for i := int32(0); i < confirmationWindow; i++ {
condition, err := checker.Condition(countNode)
if err != nil {
return ThresholdFailed, err
}
if condition {
count++
}

// Get the previous block node.
countNode = countNode.parent
}

// The state is locked in if the number of blocks in the
// period that voted for the rule change meets the
// activation threshold.
if count >= checker.RuleChangeActivationThreshold() {
state = ThresholdLockedIn
}

case ThresholdLockedIn:
// The new rule becomes active when its previous state
// was locked in.
state = ThresholdActive

// Nothing to do if the previous state is active or failed since
// they are both terminal states.
case ThresholdActive:
case ThresholdFailed:
// Based on the current state, the previous node, and the
// condition checker, transition to the next threshold state.
state, err = thresholdStateTransition(
state, prevNode, checker, confirmationWindow,
)
if err != nil {
return state, err
}

// Update the cache to avoid recalculating the state in the
Expand Down

0 comments on commit 588c071

Please sign in to comment.