diff --git a/Makefile b/Makefile index 5db0871a..6dc6d947 100644 --- a/Makefile +++ b/Makefile @@ -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.") diff --git a/blockchain/chain.go b/blockchain/chain.go index 92bfb268..4d1a8394 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/common_test.go b/blockchain/common_test.go index 8de699c4..1973689e 100644 --- a/blockchain/common_test.go +++ b/blockchain/common_test.go @@ -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 diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index 5da74a95..b96c9bd3 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. @@ -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 diff --git a/blockchain/thresholdstate_test.go b/blockchain/thresholdstate_test.go index c65f5a44..8d527137 100644 --- a/blockchain/thresholdstate_test.go +++ b/blockchain/thresholdstate_test.go @@ -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) + } + } +} diff --git a/blockchain/versionbits.go b/blockchain/versionbits.go index 28fcde7b..0d1f898c 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 @@ -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. // diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 8062d9d9..7b771b12 100644 --- a/btcjson/chainsvrresults.go +++ b/btcjson/chainsvrresults.go @@ -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. diff --git a/chaincfg/deployment_time_frame.go b/chaincfg/deployment_time_frame.go new file mode 100644 index 00000000..f26d4290 --- /dev/null +++ b/chaincfg/deployment_time_frame.go @@ -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) diff --git a/chaincfg/params.go b/chaincfg/params.go index a6d8d3e5..c8ddc85d 100644 --- a/chaincfg/params.go +++ b/chaincfg/params.go @@ -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 + ), }, }, diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index 9bdec34f..67b15f3a 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -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<