From dc4dc15d8c89c019847a2bb8df2833825e8403c4 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 19 Mar 2021 17:12:47 -0700 Subject: [PATCH 1/8] chaincfg: create new abstract deployment starter/ender interfaces In this commit, we create a series of new interfaces that'll allow us to abstract "when" exactly a deployment starts and ends. As is, all deployments start/end based on a unix timestamp, which is compared against the MTP of a given block to determine if a new deployment has started or ended. This works fine for BIP 9 which uses time based timeouts, but not so much for BIP 8. In order to prep a future refactor that allows our version bits implementation to support both time and block based start/end times, this new abstraction has been introduced. --- chaincfg/deployment_time_frame.go | 181 ++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 chaincfg/deployment_time_frame.go diff --git a/chaincfg/deployment_time_frame.go b/chaincfg/deployment_time_frame.go new file mode 100644 index 0000000000..7178d9b26d --- /dev/null +++ b/chaincfg/deployment_time_frame.go @@ -0,0 +1,181 @@ +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. +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. +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) From 2b6370dfd73c010c693270d74fd49cbfde72a234 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 19 Mar 2021 17:16:26 -0700 Subject: [PATCH 2/8] chaincfg: use DeploymentStarter/DeploymentEnder instead of start/end times In this commit, we utilize the recently added ConsensusDeploymentStarter and ConsensusDeploymentEnder interfaces. Concrete implementations of this interface based on the median time past comparison are used now in the ConsensusDeployment struct instead of hard coded start/end times. Along the way, we had to switch to using the "zero time": time.Time{}, in place of 0 and math.MaxInt64 as comparison (After/Before) seems to be broken in the Go stdlib for times very far in the future. It appears Go isn't ready to handle the heat death of the universe. --- chaincfg/deployment_time_frame.go | 8 +- chaincfg/params.go | 173 ++++++++++++++++++++---------- rpcserver.go | 13 ++- 3 files changed, 134 insertions(+), 60 deletions(-) diff --git a/chaincfg/deployment_time_frame.go b/chaincfg/deployment_time_frame.go index 7178d9b26d..f26d429090 100644 --- a/chaincfg/deployment_time_frame.go +++ b/chaincfg/deployment_time_frame.go @@ -80,7 +80,9 @@ type MedianTimeDeploymentStarter struct { } // NewMedianTimeDeploymentStarter returns a new instance of a -// MedianTimeDeploymentStarter for a given start time. +// 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, @@ -134,7 +136,9 @@ type MedianTimeDeploymentEnder struct { } // NewMedianTimeDeploymentEnder returns a new instance of the -// MedianTimeDeploymentEnder anchored around the passed endTime. +// 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, diff --git a/chaincfg/params.go b/chaincfg/params.go index a6d8d3e551..468106d5e7 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,13 @@ 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 + // DeploymentStarter is used to determine if the given + // ConsensusDeployment has started or not. + DeploymentStarter ConsensusDeploymentStarter - // ExpireTime is the median block time after which the attempted - // deployment expires. - ExpireTime uint64 + // 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 @@ -320,19 +319,31 @@ 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 + ), }, 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 +407,31 @@ 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 + ), }, 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 +513,31 @@ 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 + ), }, 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 +605,31 @@ 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 + ), }, 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 +712,40 @@ 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 + ), }, 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. + BitNumber: 29, + DeploymentStarter: NewMedianTimeDeploymentStarter( + time.Time{}, // Always available for vote + ), + DeploymentEnder: NewMedianTimeDeploymentEnder( + time.Time{}, // 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/rpcserver.go b/rpcserver.go index 8a4ecaac8d..1dd0659677 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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" ) @@ -1289,11 +1289,18 @@ 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), + StartTime2: startTime, + Timeout: endTime, } } From 298d6165be603b0efcd9d4ec48508fd1e0fad3fa Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 19 Mar 2021 17:18:57 -0700 Subject: [PATCH 3/8] blockchain: update version bits logic to use HasStarted/HasEnded for deployments In this commit, we update our version bits logic to use the newly added HasStarted and HasEnded methods for consensus deployments. Along the way, wee modify the thresholdConditionChecker` interface to be based off the new chaincfg interfaces. In addition, we add a new method `PastMedianTime`, in order to allow the chain itself to be used as a `chaincfg.BlockClock`. This serves to make the logic more generic in order to support both block height and time based soft fork timeouts. --- blockchain/chain.go | 16 ++++++++++- blockchain/common_test.go | 18 +++++++++++-- blockchain/thresholdstate.go | 51 ++++++++++++++++++++++------------- blockchain/versionbits.go | 52 ++++++++++++++++++++---------------- 4 files changed, 93 insertions(+), 44 deletions(-) diff --git a/blockchain/chain.go b/blockchain/chain.go index 92bfb26876..4d1a839441 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 8de699c416..1973689ea1 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 5da74a95af..6f3841bbe1 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. @@ -121,6 +122,27 @@ func newThresholdCaches(numCaches uint32) []thresholdStateCache { return caches } +// PastMedianTime returns the past median time from the PoV of the passed block +// header. The past median time is the median time of the 11 blocks prior to +// the passed block header. +// +// NOTE: This is part of the chainfg.BlockClock interface +func (b *BlockChain) PastMedianTime(blockHeader *wire.BlockHeader) (time.Time, error) { + prevHash := blockHeader.PrevBlock + prevNode := b.index.LookupNode(&prevHash) + + // If we can't find the previous node, then we can't compute the block + // time since it requires us to walk backwards from this node. + if prevNode == nil { + return time.Time{}, fmt.Errorf("blockHeader(%v) has no "+ + "previous node", blockHeader.BlockHash()) + } + + blockNode := newBlockNode(blockHeader, prevNode) + + return blockNode.CalcPastMedianTime(), nil +} + // thresholdState returns the current rule change threshold state for the block // AFTER the given node and deployment ID. The cache is used to ensure the // threshold states for previous windows are only calculated once. @@ -150,13 +172,9 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit break } - // The start and expiration times are based on the median block - // time, so calculate it now. - medianTime := prevNode.CalcPastMedianTime() - // The state is simply defined if the start time hasn't been // been reached yet. - if uint64(medianTime.Unix()) < checker.BeginTime() { + if !checker.HasStarted(prevNode) { cache.Update(&prevNode.hash, ThresholdDefined) break } @@ -192,9 +210,7 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit case ThresholdDefined: // The deployment of the rule change fails if it expires // before it is accepted and locked in. - medianTime := prevNode.CalcPastMedianTime() - medianTimeUnix := uint64(medianTime.Unix()) - if medianTimeUnix >= checker.EndTime() { + if checker.HasEnded(prevNode) { state = ThresholdFailed break } @@ -202,15 +218,14 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit // The state for the rule moves to the started state // once its start time has been reached (and it hasn't // already expired per the above). - if medianTimeUnix >= checker.BeginTime() { + if checker.HasStarted(prevNode) { state = ThresholdStarted } case ThresholdStarted: // The deployment of the rule change fails if it expires // before it is accepted and locked in. - medianTime := prevNode.CalcPastMedianTime() - if uint64(medianTime.Unix()) >= checker.EndTime() { + if checker.HasEnded(prevNode) { state = ThresholdFailed break } diff --git a/blockchain/versionbits.go b/blockchain/versionbits.go index 28fcde7b69..dbe4eb1d61 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 @@ -123,27 +120,36 @@ type deploymentChecker struct { // interface. var _ thresholdConditionChecker = deploymentChecker{} -// BeginTime returns the unix timestamp for the median block time after which -// voting on a rule change starts (at the next window). +// HasEnded returns true if the target consensus rule change has expired +// or timed out (at the next window). // // This implementation returns the value defined by the specific deployment the // checker is associated with. // // This is part of the thresholdConditionChecker interface implementation. -func (c deploymentChecker) BeginTime() uint64 { - return c.deployment.StartTime +func (c deploymentChecker) HasStarted(blkNode *blockNode) bool { + // Can't fail as we make sure to set the clock above when we + // instantiate *BlockChain. + header := blkNode.Header() + started, _ := c.deployment.DeploymentStarter.HasStarted(&header) + + return started } -// EndTime returns the unix timestamp for the median block time after which an -// attempted rule change fails if it has not already been locked in or -// activated. +// HasEnded returns true if the target consensus rule change has expired +// or timed out. // // This implementation returns the value defined by the specific deployment the // checker is associated with. // // This is part of the thresholdConditionChecker interface implementation. -func (c deploymentChecker) EndTime() uint64 { - return c.deployment.ExpireTime +func (c deploymentChecker) HasEnded(blkNode *blockNode) bool { + // Can't fail as we make sure to set the clock above when we + // instantiate *BlockChain. + header := blkNode.Header() + ended, _ := c.deployment.DeploymentEnder.HasEnded(&header) + + return ended } // RuleChangeActivationThreshold is the number of blocks for which the condition From 0556c7084f37418e1223b6e9e2b79a0c86591c95 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 11 Jan 2022 19:58:44 -0800 Subject: [PATCH 4/8] build: don't run the integration tests w/ -race --- Makefile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 5db0871ad1..6dc6d9471b 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.") From 38737a8ae35cce8f63f2d655c8f3bf02de38036a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 13 Jan 2022 17:29:54 -0800 Subject: [PATCH 5/8] chainparams: add new DeploymentTestDummyMinActivation In this commit, we add a new "dummy" deployment that adds the new params used to activate taproot. We chose to add a new deployment as unlike the bitcoind codebase, we don't currently "bury" soft forks that have happened in the past (hard code an activation height). The old taproot deployment has been removed as with the way the array works, a deployment needs to be defined for _all_ networks. --- chaincfg/params.go | 78 ++++++++++++++++++++++++++++++++++++++++------ rpcserver.go | 6 ++-- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/chaincfg/params.go b/chaincfg/params.go index 468106d5e7..c8ddc85d69 100644 --- a/chaincfg/params.go +++ b/chaincfg/params.go @@ -94,6 +94,19 @@ type ConsensusDeployment struct { // this particular soft-fork deployment refers to. BitNumber uint8 + // 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 + + // 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 @@ -111,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. @@ -121,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. @@ -327,6 +341,17 @@ var MainNetParams = Params{ 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, DeploymentStarter: NewMedianTimeDeploymentStarter( @@ -415,6 +440,17 @@ var RegressionNetParams = Params{ 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, DeploymentStarter: NewMedianTimeDeploymentStarter( @@ -521,6 +557,17 @@ var TestNet3Params = Params{ 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, DeploymentStarter: NewMedianTimeDeploymentStarter( @@ -613,6 +660,17 @@ var SimNetParams = Params{ 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, DeploymentStarter: NewMedianTimeDeploymentStarter( @@ -720,8 +778,10 @@ func CustomSignetParams(challenge []byte, dnsSeeds []DNSSeed) Params { time.Unix(1230767999, 0), // December 31, 2008 UTC ), }, - DeploymentCSV: { - BitNumber: 29, + 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 ), @@ -729,7 +789,7 @@ func CustomSignetParams(challenge []byte, dnsSeeds []DNSSeed) Params { time.Time{}, // Never expires ), }, - DeploymentSegwit: { + DeploymentCSV: { BitNumber: 29, DeploymentStarter: NewMedianTimeDeploymentStarter( time.Time{}, // Always available for vote @@ -738,7 +798,7 @@ func CustomSignetParams(challenge []byte, dnsSeeds []DNSSeed) Params { time.Time{}, // Never expires ), }, - DeploymentTaproot: { + DeploymentSegwit: { BitNumber: 29, DeploymentStarter: NewMedianTimeDeploymentStarter( time.Time{}, // Always available for vote diff --git a/rpcserver.go b/rpcserver.go index 1dd0659677..8c4b765266 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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, From c6b66ee79c77f3ca41de8844de14ade81d0a1b19 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 13 Jan 2022 17:42:07 -0800 Subject: [PATCH 6/8] blockchain+integration: add support for min activation height and custom thresholds In this commit, we extend the existing version bits state machine to add support for the new minimum activation height and custom block threshold for activation. We then extend the existing BIP 9 tests (tho this isn't really BIP 9 anymore...) to exercise the new min activation height logic. --- blockchain/thresholdstate.go | 68 +++++++++++++++++++++++++++--------- blockchain/versionbits.go | 63 +++++++++++++++++++++++++++++++++ integration/bip0009_test.go | 44 +++++++++++++++++++++-- 3 files changed, 157 insertions(+), 18 deletions(-) diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index 6f3841bbe1..29b62e468f 100644 --- a/blockchain/thresholdstate.go +++ b/blockchain/thresholdstate.go @@ -72,8 +72,8 @@ type thresholdConditionChecker interface { // consensus is eligible for deployment. HasStarted(*blockNode) bool - // HasEnded returns true if the target consensus rule change has expired - // or timed out. + // 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 @@ -84,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) } @@ -208,9 +221,11 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit switch state { case ThresholdDefined: - // The deployment of the rule change fails if it expires - // before it is accepted and locked in. - if checker.HasEnded(prevNode) { + // 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 } @@ -223,9 +238,10 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit } case ThresholdStarted: - // The deployment of the rule change fails if it expires - // before it is accepted and locked in. - if checker.HasEnded(prevNode) { + // 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 } @@ -248,17 +264,37 @@ func (b *BlockChain) thresholdState(prevNode *blockNode, checker thresholdCondit 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. - if count >= checker.RuleChangeActivationThreshold() { + 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: - // The new rule becomes active when its previous state - // was locked in. - state = ThresholdActive + // 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. diff --git a/blockchain/versionbits.go b/blockchain/versionbits.go index dbe4eb1d61..0d1f898c0a 100644 --- a/blockchain/versionbits.go +++ b/blockchain/versionbits.go @@ -108,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. @@ -160,6 +186,12 @@ func (c deploymentChecker) HasEnded(blkNode *blockNode) bool { // // 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 } @@ -174,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/integration/bip0009_test.go b/integration/bip0009_test.go index 9bdec34fbb..67b15f3a5b 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< Date: Sun, 23 Jan 2022 19:06:21 -0800 Subject: [PATCH 7/8] blockchain: refactor new thresholdState method, test BIP9 transitions In this commit, we extract the BIP 9 state transition logic from the thresholdState method into a new thresholdStateTransition function that allows us to test all the defined state transitions, including the modified "speedy trial" logic. --- blockchain/thresholdstate.go | 182 ++++++++++++++++------------- blockchain/thresholdstate_test.go | 184 ++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+), 81 deletions(-) diff --git a/blockchain/thresholdstate.go b/blockchain/thresholdstate.go index 29b62e468f..b96c9bd3db 100644 --- a/blockchain/thresholdstate.go +++ b/blockchain/thresholdstate.go @@ -156,6 +156,99 @@ func (b *BlockChain) PastMedianTime(blockHeader *wire.BlockHeader) (time.Time, e 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. @@ -216,90 +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. 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: + // 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 c65f5a4465..8d527137e3 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) + } + } +} From 0b245cca4f4c0d091a4466a77b9de70523dc1af0 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 13 Jan 2022 17:51:30 -0800 Subject: [PATCH 8/8] btcjson+rpc: add min activation height to soft fork RPC response --- btcjson/chainsvrresults.go | 15 ++++++++------- rpcserver.go | 9 +++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/btcjson/chainsvrresults.go b/btcjson/chainsvrresults.go index 8062d9d990..7b771b12f7 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/rpcserver.go b/rpcserver.go index 8c4b765266..e000af5a4f 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1297,10 +1297,11 @@ func handleGetBlockChainInfo(s *rpcServer, cmd interface{}, closeChan <-chan str endTime = ender.EndTime().Unix() } chainInfo.SoftForks.Bip9SoftForks[forkName] = &btcjson.Bip9SoftForkDescription{ - Status: strings.ToLower(statusString), - Bit: deploymentDetails.BitNumber, - StartTime2: startTime, - Timeout: endTime, + Status: strings.ToLower(statusString), + Bit: deploymentDetails.BitNumber, + StartTime2: startTime, + Timeout: endTime, + MinActivationHeight: int32(deploymentDetails.MinActivationHeight), } }