Merge pull request #1700 from Roasbeef/bip-8-prep

chaincfg+blockchain: abstract/refactor BIP 9 version bits implementation to work w/ BIP 8 block heights
This commit is contained in:
Olaoluwa Osuntokun 2022-01-26 12:53:38 -08:00 committed by GitHub
commit 588c0714c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 896 additions and 186 deletions

View file

@ -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/*")
@ -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.")

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

View file

@ -132,3 +132,187 @@ nextTest:
}
}
}
type customDeploymentChecker struct {
started bool
ended bool
eligible bool
isSpeedy bool
conditionTrue bool
activationThreshold uint32
minerWindow uint32
}
func (c customDeploymentChecker) HasStarted(_ *blockNode) bool {
return c.started
}
func (c customDeploymentChecker) HasEnded(_ *blockNode) bool {
return c.ended
}
func (c customDeploymentChecker) RuleChangeActivationThreshold() uint32 {
return c.activationThreshold
}
func (c customDeploymentChecker) MinerConfirmationWindow() uint32 {
return c.minerWindow
}
func (c customDeploymentChecker) EligibleToActivate(_ *blockNode) bool {
return c.eligible
}
func (c customDeploymentChecker) IsSpeedy() bool {
return c.isSpeedy
}
func (c customDeploymentChecker) Condition(_ *blockNode) (bool, error) {
return c.conditionTrue, nil
}
// TestThresholdStateTransition tests that the thresholdStateTransition
// properly implements the BIP 009 state machine, along with the speedy trial
// augments.
func TestThresholdStateTransition(t *testing.T) {
t.Parallel()
// Prev node always points back to itself, effectively creating an
// infinite chain for the purposes of this test.
prevNode := &blockNode{}
prevNode.parent = prevNode
window := int32(2016)
testCases := []struct {
currentState ThresholdState
nextState ThresholdState
checker thresholdConditionChecker
}{
// From defined, we stay there if we haven't started the
// window, and the window hasn't ended.
{
currentState: ThresholdDefined,
nextState: ThresholdDefined,
checker: &customDeploymentChecker{},
},
// From defined, we go to failed if the window has ended, and
// this isn't a speedy trial.
{
currentState: ThresholdDefined,
nextState: ThresholdFailed,
checker: &customDeploymentChecker{
ended: true,
},
},
// From defined, even if the window has ended, we go to started
// if this isn't a speedy trial.
{
currentState: ThresholdDefined,
nextState: ThresholdStarted,
checker: &customDeploymentChecker{
started: true,
},
},
// From started, we go to failed if this isn't speed, and the
// deployment has ended.
{
currentState: ThresholdStarted,
nextState: ThresholdFailed,
checker: &customDeploymentChecker{
ended: true,
},
},
// From started, we go to locked in if the window passed the
// condition.
{
currentState: ThresholdStarted,
nextState: ThresholdLockedIn,
checker: &customDeploymentChecker{
started: true,
conditionTrue: true,
},
},
// From started, we go to failed if this is a speedy trial, and
// the condition wasn't met in the window.
{
currentState: ThresholdStarted,
nextState: ThresholdFailed,
checker: &customDeploymentChecker{
started: true,
ended: true,
isSpeedy: true,
conditionTrue: false,
activationThreshold: 1815,
},
},
// From locked in, we go straight to active is this isn't a
// speedy trial.
{
currentState: ThresholdLockedIn,
nextState: ThresholdActive,
checker: &customDeploymentChecker{
eligible: true,
},
},
// From locked in, we remain in locked in if we're not yet
// eligible to activate.
{
currentState: ThresholdLockedIn,
nextState: ThresholdLockedIn,
checker: &customDeploymentChecker{},
},
// From active, we always stay here.
{
currentState: ThresholdActive,
nextState: ThresholdActive,
checker: &customDeploymentChecker{},
},
// From failed, we always stay here.
{
currentState: ThresholdFailed,
nextState: ThresholdFailed,
checker: &customDeploymentChecker{},
},
}
for i, testCase := range testCases {
nextState, err := thresholdStateTransition(
testCase.currentState, prevNode, testCase.checker,
window,
)
if err != nil {
t.Fatalf("#%v: unable to transition to next "+
"state: %v", i, err)
}
if nextState != testCase.nextState {
t.Fatalf("#%v: incorrect state transition: "+
"expected %v got %v", i, testCase.nextState,
nextState)
}
}
}

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
@ -111,6 +108,32 @@ func (c bitConditionChecker) Condition(node *blockNode) (bool, error) {
return uint32(expectedVersion)&conditionMask == 0, nil
}
// 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.
//
// This implementation always returns true, as it's used to warn about other
// unknown deployments.
//
// This is part of the thresholdConditionChecker interface implementation.
func (c bitConditionChecker) EligibleToActivate(blkNode *blockNode) bool {
return true
}
// 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.
//
// This implementation returns false, as we want to always be warned if
// something is about to activate.
//
// This is part of the thresholdConditionChecker interface implementation.
func (c bitConditionChecker) IsSpeedy() bool {
return false
}
// deploymentChecker provides a thresholdConditionChecker which can be used to
// test a specific deployment rule. This is required for properly detecting
// and activating consensus rule changes.
@ -123,27 +146,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
@ -154,6 +186,12 @@ func (c deploymentChecker) EndTime() uint64 {
//
// This is part of the thresholdConditionChecker interface implementation.
func (c deploymentChecker) RuleChangeActivationThreshold() uint32 {
// Some deployments like taproot used a custom activation threshold
// that ovverides the network level threshold.
if c.deployment.CustomActivationThreshold != 0 {
return c.deployment.CustomActivationThreshold
}
return c.chain.chainParams.RuleChangeActivationThreshold
}
@ -168,6 +206,37 @@ func (c deploymentChecker) MinerConfirmationWindow() uint32 {
return c.chain.chainParams.MinerConfirmationWindow
}
// 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.
//
// This implementation always returns true, unless a minimum activation height
// is specified.
//
// This is part of the thresholdConditionChecker interface implementation.
func (c deploymentChecker) EligibleToActivate(blkNode *blockNode) bool {
// No activation height, so it's always ready to go.
if c.deployment.MinActivationHeight == 0 {
return true
}
// If the _next_ block (as this is the prior block to the one being
// connected is the min height or beyond, then this can activate.
return uint32(blkNode.height)+1 >= c.deployment.MinActivationHeight
}
// 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. This implementation returns
// true if a min activation height is set.
//
// This is part of the thresholdConditionChecker interface implementation.
func (c deploymentChecker) IsSpeedy() bool {
return c.deployment.MinActivationHeight != 0
}
// Condition returns true when the specific bit defined by the deployment
// associated with the checker is set.
//

View file

@ -11,8 +11,8 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
)
// GetBlockHeaderVerboseResult models the data from the getblockheader command when
@ -172,12 +172,13 @@ type SoftForkDescription struct {
// Bip9SoftForkDescription describes the current state of a defined BIP0009
// version bits soft-fork.
type Bip9SoftForkDescription struct {
Status string `json:"status"`
Bit uint8 `json:"bit"`
StartTime1 int64 `json:"startTime"`
StartTime2 int64 `json:"start_time"`
Timeout int64 `json:"timeout"`
Since int32 `json:"since"`
Status string `json:"status"`
Bit uint8 `json:"bit"`
StartTime1 int64 `json:"startTime"`
StartTime2 int64 `json:"start_time"`
Timeout int64 `json:"timeout"`
Since int32 `json:"since"`
MinActivationHeight int32 `json:"min_activation_height"`
}
// StartTime returns the starting time of the softfork as a Unix epoch.

View file

@ -0,0 +1,185 @@
package chaincfg
import (
"fmt"
"time"
"github.com/btcsuite/btcd/wire"
)
var (
// ErrNoBlockClock is returned when an operation fails due to lack of
// synchornization with the current up to date block clock.
ErrNoBlockClock = fmt.Errorf("no block clock synchronized")
)
// BlockClock is an abstraction over the past median time computation. The past
// median time computation is used in several consensus checks such as CSV, and
// also BIP 9 version bits. This interface allows callers to abstract away the
// computation of the past median time from the perspective of a given block
// header.
type BlockClock interface {
// 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.
PastMedianTime(*wire.BlockHeader) (time.Time, error)
}
// ConsensusDeploymentStarter determines if a given consensus deployment has
// started. A deployment has started once according to the current "time", the
// deployment is eligible for activation once a perquisite condition has
// passed.
type ConsensusDeploymentStarter interface {
// HasStarted returns true if the consensus deployment has started.
HasStarted(*wire.BlockHeader) (bool, error)
}
// ClockConsensusDeploymentStarter is a more specialized version of the
// ConsensusDeploymentStarter that uses a BlockClock in order to determine if a
// deployment has started or not.
//
// NOTE: Any calls to HasStarted will _fail_ with ErrNoBlockClock if they
// happen before SynchronizeClock is executed.
type ClockConsensusDeploymentStarter interface {
ConsensusDeploymentStarter
// SynchronizeClock synchronizes the target ConsensusDeploymentStarter
// with the current up-to date BlockClock.
SynchronizeClock(clock BlockClock)
}
// ConsensusDeploymentEnder determines if a given consensus deployment has
// ended. A deployment has ended once according got eh current "time", the
// deployment is no longer eligible for activation.
type ConsensusDeploymentEnder interface {
// HasEnded returns true if the consensus deployment has ended.
HasEnded(*wire.BlockHeader) (bool, error)
}
// ClockConsensusDeploymentEnder is a more specialized version of the
// ConsensusDeploymentEnder that uses a BlockClock in order to determine if a
// deployment has started or not.
//
// NOTE: Any calls to HasEnded will _fail_ with ErrNoBlockClock if they
// happen before SynchronizeClock is executed.
type ClockConsensusDeploymentEnder interface {
ConsensusDeploymentEnder
// SynchronizeClock synchronizes the target ConsensusDeploymentStarter
// with the current up-to date BlockClock.
SynchronizeClock(clock BlockClock)
}
// MedianTimeDeploymentStarter is a ClockConsensusDeploymentStarter that uses
// the median time past of a target block node to determine if a deployment has
// started.
type MedianTimeDeploymentStarter struct {
blockClock BlockClock
startTime time.Time
}
// NewMedianTimeDeploymentStarter returns a new instance of a
// MedianTimeDeploymentStarter for a given start time. Using a time.Time
// instance where IsZero() is true, indicates that a deployment should be
// considered to always have been started.
func NewMedianTimeDeploymentStarter(startTime time.Time) *MedianTimeDeploymentStarter {
return &MedianTimeDeploymentStarter{
startTime: startTime,
}
}
// SynchronizeClock synchronizes the target ConsensusDeploymentStarter with the
// current up-to date BlockClock.
func (m *MedianTimeDeploymentStarter) SynchronizeClock(clock BlockClock) {
m.blockClock = clock
}
// HasStarted returns true if the consensus deployment has started.
func (m *MedianTimeDeploymentStarter) HasStarted(blkHeader *wire.BlockHeader) (bool, error) {
switch {
// If we haven't yet been synchronized with a block clock, then we
// can't tell the time, so we'll fail.
case m.blockClock == nil:
return false, ErrNoBlockClock
// If the time is "zero", then the deployment has always started.
case m.startTime.IsZero():
return true, nil
}
medianTime, err := m.blockClock.PastMedianTime(blkHeader)
if err != nil {
return false, err
}
// We check both after and equal here as after will fail for equivalent
// times, and we want to be inclusive.
return medianTime.After(m.startTime) || medianTime.Equal(m.startTime), nil
}
// StartTime returns the raw start time of the deployment.
func (m *MedianTimeDeploymentStarter) StartTime() time.Time {
return m.startTime
}
// A compile-time assertion to ensure MedianTimeDeploymentStarter implements
// the ClockConsensusDeploymentStarter interface.
var _ ClockConsensusDeploymentStarter = (*MedianTimeDeploymentStarter)(nil)
// MedianTimeDeploymentEnder is a ClockConsensusDeploymentEnder that uses the
// median time past of a target block to determine if a deployment has ended.
type MedianTimeDeploymentEnder struct {
blockClock BlockClock
endTime time.Time
}
// NewMedianTimeDeploymentEnder returns a new instance of the
// MedianTimeDeploymentEnder anchored around the passed endTime. Using a
// time.Time instance where IsZero() is true, indicates that a deployment
// should be considered to never end.
func NewMedianTimeDeploymentEnder(endTime time.Time) *MedianTimeDeploymentEnder {
return &MedianTimeDeploymentEnder{
endTime: endTime,
}
}
// HasEnded returns true if the deployment has ended.
func (m *MedianTimeDeploymentEnder) HasEnded(blkHeader *wire.BlockHeader) (bool, error) {
switch {
// If we haven't yet been synchronized with a block clock, then we can't tell
// the time, so we'll we haven't yet been synchronized with a block
// clock, then w can't tell the time, so we'll fail.
case m.blockClock == nil:
return false, ErrNoBlockClock
// If the time is "zero", then the deployment never ends.
case m.endTime.IsZero():
return false, nil
}
medianTime, err := m.blockClock.PastMedianTime(blkHeader)
if err != nil {
return false, err
}
// We check both after and equal here as after will fail for equivalent
// times, and we want to be inclusive.
return medianTime.After(m.endTime) || medianTime.Equal(m.endTime), nil
}
// MedianTimeDeploymentEnder returns the raw end time of the deployment.
func (m *MedianTimeDeploymentEnder) EndTime() time.Time {
return m.endTime
}
// SynchronizeClock synchronizes the target ConsensusDeploymentEnder with the
// current up-to date BlockClock.
func (m *MedianTimeDeploymentEnder) SynchronizeClock(clock BlockClock) {
m.blockClock = clock
}
// A compile-time assertion to ensure MedianTimeDeploymentEnder implements the
// ClockConsensusDeploymentStarter interface.
var _ ClockConsensusDeploymentEnder = (*MedianTimeDeploymentEnder)(nil)

View file

@ -8,7 +8,6 @@ import (
"encoding/binary"
"encoding/hex"
"errors"
"math"
"math/big"
"strings"
"time"
@ -95,13 +94,26 @@ type ConsensusDeployment struct {
// this particular soft-fork deployment refers to.
BitNumber uint8
// StartTime is the median block time after which voting on the
// deployment starts.
StartTime uint64
// MinActivationHeight is an optional field that when set (default
// value being zero), modifies the traditional BIP 9 state machine by
// only transitioning from LockedIn to Active once the block height is
// greater than (or equal to) thus specified height.
MinActivationHeight uint32
// ExpireTime is the median block time after which the attempted
// deployment expires.
ExpireTime uint64
// CustomActivationThreshold if set (non-zero), will _override_ the
// existing RuleChangeActivationThreshold value set at the
// network/chain level. This value divided by the active
// MinerConfirmationWindow denotes the threshold required for
// activation. A value of 1815 block denotes a 90% threshold.
CustomActivationThreshold uint32
// DeploymentStarter is used to determine if the given
// ConsensusDeployment has started or not.
DeploymentStarter ConsensusDeploymentStarter
// DeploymentEnder is used to determine if the given
// ConsensusDeployment has ended or not.
DeploymentEnder ConsensusDeploymentEnder
}
// Constants that define the deployment offset in the deployments field of the
@ -112,6 +124,12 @@ const (
// purposes.
DeploymentTestDummy = iota
// DeploymentTestDummyMinActivation defines the rule change deployment
// ID for testing purposes. This differs from the DeploymentTestDummy
// in that it specifies the newer params the taproot fork used for
// activation: a custom threshold and a min activation height.
DeploymentTestDummyMinActivation
// DeploymentCSV defines the rule change deployment ID for the CSV
// soft-fork package. The CSV package includes the deployment of BIPS
// 68, 112, and 113.
@ -122,11 +140,6 @@ const (
// includes the deployment of BIPS 141, 142, 144, 145, 147 and 173.
DeploymentSegwit
// DeploymentTaproot defines the rule change deployment ID for the
// Taproot (+Schnorr) soft-fork package. The taproot package includes
// the deployment of BIPS 340, 341 and 342.
DeploymentTaproot
// NOTE: DefinedDeployments must always come last since it is used to
// determine how many defined deployments there currently are.
@ -320,19 +333,42 @@ var MainNetParams = Params{
MinerConfirmationWindow: 2016, //
Deployments: [DefinedDeployments]ConsensusDeployment{
DeploymentTestDummy: {
BitNumber: 28,
StartTime: 1199145601, // January 1, 2008 UTC
ExpireTime: 1230767999, // December 31, 2008 UTC
BitNumber: 28,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Unix(11991456010, 0), // January 1, 2008 UTC
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Unix(1230767999, 0), // December 31, 2008 UTC
),
},
DeploymentTestDummyMinActivation: {
BitNumber: 22,
CustomActivationThreshold: 1815, // Only needs 90% hash rate.
MinActivationHeight: 10_0000, // Can only activate after height 10k.
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentCSV: {
BitNumber: 0,
StartTime: 1462060800, // May 1st, 2016
ExpireTime: 1493596800, // May 1st, 2017
BitNumber: 0,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Unix(1462060800, 0), // May 1st, 2016
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Unix(1493596800, 0), // May 1st, 2017
),
},
DeploymentSegwit: {
BitNumber: 1,
StartTime: 1479168000, // November 15, 2016 UTC
ExpireTime: 1510704000, // November 15, 2017 UTC.
BitNumber: 1,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Unix(1479168000, 0), // November 15, 2016 UTC
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Unix(1510704000, 0), // November 15, 2017 UTC.
),
},
},
@ -396,19 +432,42 @@ var RegressionNetParams = Params{
MinerConfirmationWindow: 144,
Deployments: [DefinedDeployments]ConsensusDeployment{
DeploymentTestDummy: {
BitNumber: 28,
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires
BitNumber: 28,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentTestDummyMinActivation: {
BitNumber: 22,
CustomActivationThreshold: 72, // Only needs 50% hash rate.
MinActivationHeight: 600, // Can only activate after height 600.
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentCSV: {
BitNumber: 0,
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires
BitNumber: 0,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentSegwit: {
BitNumber: 1,
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires.
BitNumber: 1,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires.
),
},
},
@ -490,19 +549,42 @@ var TestNet3Params = Params{
MinerConfirmationWindow: 2016,
Deployments: [DefinedDeployments]ConsensusDeployment{
DeploymentTestDummy: {
BitNumber: 28,
StartTime: 1199145601, // January 1, 2008 UTC
ExpireTime: 1230767999, // December 31, 2008 UTC
BitNumber: 28,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Unix(1199145601, 0), // January 1, 2008 UTC
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Unix(1230767999, 0), // December 31, 2008 UTC
),
},
DeploymentTestDummyMinActivation: {
BitNumber: 22,
CustomActivationThreshold: 1815, // Only needs 90% hash rate.
MinActivationHeight: 10_0000, // Can only activate after height 10k.
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentCSV: {
BitNumber: 0,
StartTime: 1456790400, // March 1st, 2016
ExpireTime: 1493596800, // May 1st, 2017
BitNumber: 0,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Unix(1456790400, 0), // March 1st, 2016
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Unix(1493596800, 0), // May 1st, 2017
),
},
DeploymentSegwit: {
BitNumber: 1,
StartTime: 1462060800, // May 1, 2016 UTC
ExpireTime: 1493596800, // May 1, 2017 UTC.
BitNumber: 1,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Unix(1462060800, 0), // May 1, 2016 UTC
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Unix(1493596800, 0), // May 1, 2017 UTC.
),
},
},
@ -570,19 +652,42 @@ var SimNetParams = Params{
MinerConfirmationWindow: 100,
Deployments: [DefinedDeployments]ConsensusDeployment{
DeploymentTestDummy: {
BitNumber: 28,
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires
BitNumber: 28,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentTestDummyMinActivation: {
BitNumber: 22,
CustomActivationThreshold: 50, // Only needs 50% hash rate.
MinActivationHeight: 600, // Can only activate after height 600.
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentCSV: {
BitNumber: 0,
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires
BitNumber: 0,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentSegwit: {
BitNumber: 1,
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires.
BitNumber: 1,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires.
),
},
},
@ -665,24 +770,42 @@ func CustomSignetParams(challenge []byte, dnsSeeds []DNSSeed) Params {
MinerConfirmationWindow: 2016,
Deployments: [DefinedDeployments]ConsensusDeployment{
DeploymentTestDummy: {
BitNumber: 28,
StartTime: 1199145601, // January 1, 2008 UTC
ExpireTime: 1230767999, // December 31, 2008 UTC
BitNumber: 28,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Unix(1199145601, 0), // January 1, 2008 UTC
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Unix(1230767999, 0), // December 31, 2008 UTC
),
},
DeploymentTestDummyMinActivation: {
BitNumber: 22,
CustomActivationThreshold: 1815, // Only needs 90% hash rate.
MinActivationHeight: 10_0000, // Can only activate after height 10k.
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentCSV: {
BitNumber: 29,
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires
BitNumber: 29,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
DeploymentSegwit: {
BitNumber: 29,
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires.
},
DeploymentTaproot: {
BitNumber: 29,
StartTime: 0, // Always available for vote
ExpireTime: math.MaxInt64, // Never expires.
BitNumber: 29,
DeploymentStarter: NewMedianTimeDeploymentStarter(
time.Time{}, // Always available for vote
),
DeploymentEnder: NewMedianTimeDeploymentEnder(
time.Time{}, // Never expires
),
},
},

View file

@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
// This file is ignored during the regular tests due to the following build tag.
//go:build rpctest
// +build rpctest
package integration
@ -196,6 +197,9 @@ func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) {
}
deployment := &r.ActiveNet.Deployments[deploymentID]
activationThreshold := r.ActiveNet.RuleChangeActivationThreshold
if deployment.CustomActivationThreshold != 0 {
activationThreshold = deployment.CustomActivationThreshold
}
signalForkVersion := int32(1<<deployment.BitNumber) | vbTopBits
for i := uint32(0); i < activationThreshold-1; i++ {
_, err := r.GenerateAndSubmitBlock(nil, signalForkVersion,
@ -268,7 +272,42 @@ func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) {
if err != nil {
t.Fatalf("failed to generated block: %v", err)
}
assertChainHeight(r, t, (confirmationWindow*4)-1)
expectedChainHeight := (confirmationWindow * 4) - 1
assertChainHeight(r, t, expectedChainHeight)
// If this isn't a fork that has a min activation height set, then it
// should be active at this point.
if deployment.MinActivationHeight == 0 {
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdActive)
return
}
// Otherwise, we'll need to mine additional blocks to pass the min
// activation height and ensure the rule set applies. For regtest the
// deployment can only activate after height 600, and at this point
// we've mined 4*144 blocks, so another confirmation window will put us
// over.
numBlocksLeft := confirmationWindow
for i := uint32(0); i < numBlocksLeft; i++ {
// Ensure that we're always in the locked in state right up
// until after we mine the very last block.
if i < numBlocksLeft {
assertSoftForkStatus(
r, t, forkKey, blockchain.ThresholdLockedIn,
)
}
_, err := r.GenerateAndSubmitBlock(
nil, signalForkVersion, time.Time{},
)
if err != nil {
t.Fatalf("failed to generated block %d: %v", i, err)
}
}
// At this point, the soft fork should now be shown as active.
expectedChainHeight = (confirmationWindow * 5) - 1
assertChainHeight(r, t, expectedChainHeight)
assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdActive)
}
@ -299,6 +338,7 @@ func TestBIP0009(t *testing.T) {
t.Parallel()
testBIP0009(t, "dummy", chaincfg.DeploymentTestDummy)
testBIP0009(t, "dummy-min-activation", chaincfg.DeploymentTestDummyMinActivation)
testBIP0009(t, "segwit", chaincfg.DeploymentSegwit)
}
@ -329,7 +369,7 @@ func TestBIP0009Mining(t *testing.T) {
}
defer r.TearDown()
// Assert the chain only consists of the gensis block.
// Assert the chain only consists of the genesis block.
assertChainHeight(r, t, 0)
// *** ThresholdDefined ***

View file

@ -31,6 +31,7 @@ import (
"github.com/btcsuite/btcd/blockchain/indexers"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/database"
@ -40,7 +41,6 @@ import (
"github.com/btcsuite/btcd/peer"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/websocket"
)
@ -1250,15 +1250,15 @@ func handleGetBlockChainInfo(s *rpcServer, cmd interface{}, closeChan <-chan str
case chaincfg.DeploymentTestDummy:
forkName = "dummy"
case chaincfg.DeploymentTestDummyMinActivation:
forkName = "dummy-min-activation"
case chaincfg.DeploymentCSV:
forkName = "csv"
case chaincfg.DeploymentSegwit:
forkName = "segwit"
case chaincfg.DeploymentTaproot:
forkName = "taproot"
default:
return nil, &btcjson.RPCError{
Code: btcjson.ErrRPCInternal.Code,
@ -1289,11 +1289,19 @@ func handleGetBlockChainInfo(s *rpcServer, cmd interface{}, closeChan <-chan str
// Finally, populate the soft-fork description with all the
// information gathered above.
var startTime, endTime int64
if starter, ok := deploymentDetails.DeploymentStarter.(*chaincfg.MedianTimeDeploymentStarter); ok {
startTime = starter.StartTime().Unix()
}
if ender, ok := deploymentDetails.DeploymentEnder.(*chaincfg.MedianTimeDeploymentEnder); ok {
endTime = ender.EndTime().Unix()
}
chainInfo.SoftForks.Bip9SoftForks[forkName] = &btcjson.Bip9SoftForkDescription{
Status: strings.ToLower(statusString),
Bit: deploymentDetails.BitNumber,
StartTime2: int64(deploymentDetails.StartTime),
Timeout: int64(deploymentDetails.ExpireTime),
Status: strings.ToLower(statusString),
Bit: deploymentDetails.BitNumber,
StartTime2: startTime,
Timeout: endTime,
MinActivationHeight: int32(deploymentDetails.MinActivationHeight),
}
}