diff --git a/blockmanager.go b/blockmanager.go index 4df24f13..a296ee78 100644 --- a/blockmanager.go +++ b/blockmanager.go @@ -1778,6 +1778,12 @@ func (b *blockManager) handleNotifyMsg(notification *blockchain.Notification) { b.server.RelayInventory(iv, block.MsgBlock().Header, true) } + // Inform the background block template generator about the accepted + // block. + if b.server.bg != nil { + b.server.bg.BlockAccepted(block) + } + if !b.server.feeEstimator.IsEnabled() { // fee estimation can only start after we have performed an initial // sync, otherwise we'll start adding mempool transactions at the @@ -1890,7 +1896,7 @@ func (b *blockManager) handleNotifyMsg(notification *blockchain.Notification) { } if b.server.bg != nil { - b.server.bg.handleConnectedBlock(b.server.context, block.Height()) + b.server.bg.BlockConnected(block) } // Stake tickets are spent or missed from the most recently connected block. @@ -1985,7 +1991,7 @@ func (b *blockManager) handleNotifyMsg(notification *blockchain.Notification) { handleDisconnectedBlockTxns(block.STransactions()) if b.server.bg != nil { - b.server.bg.handleDisconnectedBlock(block.Height()) + b.server.bg.BlockDisconnected(block) } // Filter and update the rebroadcast inventory. @@ -2003,13 +2009,13 @@ func (b *blockManager) handleNotifyMsg(notification *blockchain.Notification) { // Chain reorganization has commenced. case blockchain.NTChainReorgStarted: if b.server.bg != nil { - b.server.bg.handleChainReorgStarted() + b.server.bg.ChainReorgStarted() } // Chain reorganization has concluded. case blockchain.NTChainReorgDone: if b.server.bg != nil { - b.server.bg.handleChainReorgDone(b.server.context) + b.server.bg.ChainReorgDone() } // The blockchain is reorganizing. diff --git a/mempool/mempool.go b/mempool/mempool.go index 3731eaad..6d672d46 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -127,7 +127,7 @@ type Config struct { // OnVoteReceived defines the function used to signal receiving a new // vote in the mempool. - OnVoteReceived func(voteTx *wire.MsgTx) + OnVoteReceived func(voteTx *dcrutil.Tx) } // Policy houses the policy (configuration parameters) which is used to @@ -242,10 +242,8 @@ type TxPool struct { // // This function MUST be called with the vote mutex locked (for writes). func (mp *TxPool) insertVote(ssgen *dcrutil.Tx) error { - msgTx := ssgen.MsgTx() - ticketHash := &msgTx.TxIn[1].PreviousOutPoint.Hash - // Get the block it is voting on; here we're agnostic of height. + msgTx := ssgen.MsgTx() blockHash, blockHeight := stake.SSGenBlockVotedOn(msgTx) // If there are currently no votes for this block, @@ -256,6 +254,7 @@ func (mp *TxPool) insertVote(ssgen *dcrutil.Tx) error { } // Nothing to do if a vote for the ticket is already known. + ticketHash := &msgTx.TxIn[1].PreviousOutPoint.Hash for _, vt := range vts { if vt.TicketHash.IsEqual(ticketHash) { return nil @@ -679,8 +678,10 @@ func (mp *TxPool) RemoveDoubleSpends(tx *dcrutil.Tx) { // This function MUST be called with the mempool lock held (for writes). func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *dcrutil.Tx, txType stake.TxType, height int64, fee int64) { + + // Notify callback about vote if requested. if mp.cfg.OnVoteReceived != nil && txType == stake.TxTypeSSGen { - mp.cfg.OnVoteReceived(tx.MsgTx()) + mp.cfg.OnVoteReceived(tx) } // Add the transaction to the pool and mark the referenced outpoints @@ -1319,7 +1320,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, allow // Add to transaction pool. mp.addTransaction(utxoView, tx, txType, bestHeight, txFee) - // Keep track of vote separately. + // Keep track of votes separately. if isVote { mp.votesMtx.Lock() err := mp.insertVote(tx) diff --git a/mining.go b/mining.go index a3477e8d..6d8d2d62 100644 --- a/mining.go +++ b/mining.go @@ -14,7 +14,6 @@ import ( "math/rand" "sort" "sync" - "sync/atomic" "time" "github.com/decred/dcrd/blockchain" @@ -51,23 +50,24 @@ const ( // kilobyte is the size of a kilobyte. kilobyte = 1000 + // minVotesTimeoutDuration is the duration that must elapse after a new tip + // block has been received before other variants that also extend the same + // parent and received later are considered for the base of new templates. + minVotesTimeoutDuration = time.Second * 3 + + // maxVoteTimeoutDuration is the duration elapsed after the minimum number + // of votes for a new tip block has been received that a new template with + // less than the maximum number of votes will be generated. + maxVoteTimeoutDuration = time.Millisecond * 2500 // 2.5 seconds + // templateRegenSecs is the required number of seconds elapsed with // incoming non vote transactions before template regeneration // is required. templateRegenSecs = 30 - // maxTemplateRegenSecs is the maximum number of seconds elapsed - // without incoming transactions before template regeneration is required. - maxTemplateRegenSecs = 60 - // merkleRootPairSize is the size in bytes of the merkle root + stake root // of a block. merkleRootPairSize = 64 - - // templateKeySize is the number of bytes for a unique block template key. - // It consists of the 32-byte merkle root and 8-byte little-endian unix - // timestamp. - templateKeySize = chainhash.HashSize + 8 // 40 bytes ) // txPrioItem houses a transaction along with extra information that allows the @@ -2039,378 +2039,1272 @@ func (g *BlkTmplGenerator) UpdateBlockTime(header *wire.BlockHeader) error { return nil } -// BgBlkTmplGenerator represents the background process that generates block -// templates and notifies all subscribed clients on template regeneration. -// It generates new templates based on the time elapsed since last template -// regeneration. -type BgBlkTmplGenerator struct { - wg sync.WaitGroup - voteChan chan *wire.MsgTx - notifyChan chan *BlockTemplate - tg *BlkTmplGenerator - txSource mining.TxSource - lastRegen int64 - subscribers map[chan *BlockTemplate]struct{} - miningAddrs []dcrutil.Address - templateMtx sync.Mutex - subscriptionMtx sync.Mutex - currentTemplate *BlockTemplate - parentTemplate *BlockTemplate - templatePool map[[templateKeySize]byte]*wire.MsgBlock - templatePoolMtx sync.RWMutex - chainReorg bool - chainReorgMtx sync.Mutex - permitConnectionlessMining bool - regenScheduler *time.Timer +// regenEventType represents the type of a template regeneration event message. +type regenEventType int + +// Constants for the type of template regeneration event messages. +const ( + // rtReorgStarted indicates a chain reorganization has been started. + rtReorgStarted regenEventType = iota + + // rtReorgDone indicates a chain reorganization has completed. + rtReorgDone + + // rtBlockAccepted indicates a new block has been accepted to the block + // chain which does not necessarily mean it was added to the main chain. + // That case is rtBlockConnected. + rtBlockAccepted + + // rtBlockConnected indicates a new block has been connected to the main + // chain. + rtBlockConnected + + // rtBlockDisconnected indicates the current tip block of the best chain has + // been disconnected. + rtBlockDisconnected + + // rtVote indicates a new vote has been received. It applies to all votes + // and therefore may or may not be relevant. + rtVote + + // rtTemplateUpdated indicates the current template associated with the + // generator has been updated. + rtTemplateUpdated +) + +// templateUpdateReason represents the type of a reason why a template is +// being updated. +type templateUpdateReason int + +// Constants for the type of template update reasons. +const ( + // turNewParent indicates the associated template has been updated because + // it builds on a new block as compared to the previous template. + turNewParent templateUpdateReason = iota + + // turNewVotes indicates the associated template has been updated because a + // new vote for the block it builds on has been received. + turNewVotes + + // turNewTxns indicates the associated template has been updated because new + // non-vote transactions are available and have potentially been included. + turNewTxns +) + +// templateUpdate defines a type which is used to signal the regen event handler +// that a new template and relevant error have been associated with the +// generator. +type templateUpdate struct { + template *BlockTemplate + err error } -// newBgBlkTmplGenerator initializes a background block template generator. -func newBgBlkTmplGenerator(tg *BlkTmplGenerator, addrs []dcrutil.Address, permitConnectionlessMining bool) *BgBlkTmplGenerator { +// regenEvent defines an event which will potentially result in regenerating a +// block template and consists of a regen event type as well as associated data +// that depends on the type as follows: +// - rtReorgStarted: nil +// - rtReorgDone: nil +// - rtBlockAccepted: *dcrutil.Block +// - rtBlockConnected: *dcrutil.Block +// - rtBlockDisconnected: *dcrutil.Block +// - rtVote: *dcrutil.Tx +// - rtTemplateUpdated: templateUpdate +type regenEvent struct { + reason regenEventType + value interface{} +} + +// BgBlkTmplGenerator provides facilities for asynchronously generating block +// templates in response to various relevant events and allowing clients to +// subscribe for updates when new templates are generated as well as access the +// most recently-generated template in a concurrency-safe manner. +// +// An example of some of the events that trigger a new block template to be +// generated are modifications to the current best chain, receiving relevant +// votes, and periodic timeouts to allow inclusion of new transactions. +// +// The templates are generated based on a given block template generator +// instance which itself is based on a given mining policy and transaction +// source. See the NewBlockTemplate method for a detailed description of how +// the block template is generated. +// +// The background generation makes use of three main goroutines -- a regen event +// queue to allow asynchronous non-blocking signalling, a regen event handler to +// process the aforementioned queue and react accordingly, and a subscriber +// notification controller. In addition, the templates themselves are generated +// in their own goroutines with a cancellable context. +// +// A high level overview of the semantics are as follows: +// - Ignore all vote handling when prior to stake validation height +// - Generate templates building on the current tip at startup with a fall back +// to generate a template on its parent if the current tip does not receive +// enough votes within a timeout +// - Continue monitoring for votes on any blocks that extend said parent to +// potentially switch to them and generate a template building on them when +// possible +// - Generate new templates building on new best chain tip blocks once they have +// received the minimum votes after a timeout to provide the additional votes +// an opportunity to propagate, except when it is an intermediate block in a +// chain reorganization +// - In the event the current tip fails to receive the minimum number of +// required votes, monitor side chain blocks which are siblings of it for +// votes in order to potentially switch to them and generate a template +// building on them when possible +// - Generate new templates on blocks disconnected from the best chain tip, +// except when it is an intermediate block in a chain reorganization +// - Generate new templates periodically when there are new regular transactions +// to include +// - Bias templates towards building on the first seen block when possible in +// order to prevent PoW miners from being able to gain an advantage through +// vote withholding +// - Schedule retries in the rare event template generation fails +// - Allow clients to subscribe for updates every time a new template is +// successfully generated along with a reason why it was generated +// - Provide direct access to the most-recently generated template +// - Block while generating new templates that will make the current template +// stale (e.g. new parent or new votes) +type BgBlkTmplGenerator struct { + wg sync.WaitGroup + prng *rand.Rand + + // These fields are provided by the caller when the generator is created and + // are either indepedently safe for concurrent access or do not change after + // initialization. + // + // chain is the blockchain instance that is used to build the block and + // validate the block templates. + // + // tg is a block template generator instance that is used to actually create + // the block templates the background block template generator stores. + // + // allowUnsyncedMining indicates block templates should be created even when + // the chain is not fully synced. + // + // maxVotesPerBlock is the maximum number of votes per block and comes from + // the chain parameters. It is defined separately for convenience. + // + // minVotesRequired is the minimum number of votes required for a block to + // be built on. It is derived from the chain parameters and is defined + // separately for convenience. + chain *blockchain.BlockChain + tg *BlkTmplGenerator + allowUnsyncedMining bool + miningAddrs []dcrutil.Address + maxVotesPerBlock uint16 + minVotesRequired uint16 + + // These fields deal with providing a stream of template updates to + // subscribers. + // + // subscriptions tracks all template update subscriptions. It is protected + // for concurrent access by subscriptionMtx. + // + // notifySubscribers delivers template updates to the separate subscriber + // notification goroutine so it can in turn asynchronously deliver + // notifications to all subscribers. + subscriptionMtx sync.Mutex + subscriptions map[*TemplateSubscription]struct{} + notifySubscribers chan *BlockTemplate + + // These fields deal with the template regeneration event queue. This is + // implemented as a concurrent queue with immediate passthrough when + // possible to ensure the order of events is maintained and the related + // callbacks never block. + // + // queueRegenEvent either immediately forwards regen events to the + // regenEventMsgs channel when it would not block or adds the event to a + // queue that is processed asynchronously as soon as the receiver becomes + // available. + // + // regenEventMsgs delivers relevant regen events to which the generator + // reacts to the separate regen goroutine so it can in turn asynchronously + // process the events and regenerate templates as needed. + queueRegenEvent chan regenEvent + regenEventMsgs chan regenEvent + + // staleTemplateWg is used to allow template retrieval to block callers when + // a new template that will make the current template stale is being + // generated. Stale, in this context, means either the parent has changed + // or there are new votes available. + staleTemplateWg sync.WaitGroup + + // These fields track the current best template and are protected by the + // template mutex. The template will be nil when there is a template error + // set. + templateMtx sync.Mutex + template *BlockTemplate + templateErr error + + // These fields are used to provide the ability to cancel a template that + // is in the process of being asynchronously generated in favor of + // generating a new one. + // + // cancelTemplate is a function which will cancel the current template that + // is in the process of being asynchronously generated. It will have no + // effect if no template generation is in progress. It is protected for + // concurrent access by cancelTemplateMtx. + cancelTemplateMtx sync.Mutex + cancelTemplate func() +} + +// newBgBlkTmplGenerator initializes a background block template generator with +// the provided parameters. The returned instance must be started with the Run +// method to allowing processing. +func newBgBlkTmplGenerator(tg *BlkTmplGenerator, addrs []dcrutil.Address, allowUnsynced bool) *BgBlkTmplGenerator { return &BgBlkTmplGenerator{ - voteChan: make(chan *wire.MsgTx), - notifyChan: make(chan *BlockTemplate), - tg: tg, - txSource: tg.txSource, - miningAddrs: addrs, - subscribers: make(map[chan *BlockTemplate]struct{}), - templatePool: make(map[[templateKeySize]byte]*wire.MsgBlock), - permitConnectionlessMining: permitConnectionlessMining, + prng: rand.New(rand.NewSource(time.Now().Unix())), + chain: tg.chain, + tg: tg, + allowUnsyncedMining: allowUnsynced, + miningAddrs: addrs, + maxVotesPerBlock: tg.chainParams.TicketsPerBlock, + minVotesRequired: (tg.chainParams.TicketsPerBlock / 2) + 1, + subscriptions: make(map[*TemplateSubscription]struct{}), + notifySubscribers: make(chan *BlockTemplate), + queueRegenEvent: make(chan regenEvent), + regenEventMsgs: make(chan regenEvent), + cancelTemplate: func() {}, } } -// templateKey returns a key that is to be used as a unique identifier for a -// block template. The key is composed of the 32-byte merkle root followed by -// the timestamp encoded as a little-endian 64-bit unsigned integer. -func templateKey(header *wire.BlockHeader) [templateKeySize]byte { - var key [templateKeySize]byte - copy(key[:], header.MerkleRoot[:]) - littleEndian.PutUint64(key[chainhash.HashSize:], - uint64(header.Timestamp.Unix())) - return key +// setCurrentTemplate sets the current template and error associated with the +// background block template generator and notifies the regen event handler +// about the update. +// +// This function is safe for concurrent access. +func (g *BgBlkTmplGenerator) setCurrentTemplate(template *BlockTemplate, err error) { + g.templateMtx.Lock() + g.template, g.templateErr = template, err + g.templateMtx.Unlock() + + tplUpdate := templateUpdate{template: template, err: err} + g.queueRegenEvent <- regenEvent{rtTemplateUpdated, tplUpdate} } -// scheduleRegen schedules a template regeneration by the provided duration -// in the future. If there is an existing schedule in play the sheduled time -// reset to updated the provided duration. -func (g *BgBlkTmplGenerator) scheduleRegen(ctx context.Context, dt time.Duration) { - if g.regenScheduler == nil { - g.regenScheduler = time.AfterFunc(dt, func() { - go g.regenTemplate(ctx) - g.cancelScheduledRegen() - }) +// CurrentTemplate returns the current template associated with the background +// template generator along with any associated error. +// +// NOTE: The returned template and block that it contains MUST be treated as +// immutable since they are shared by all callers. +// +// NOTE: The returned template might be nil even if there is no error. It is +// the responsibility of the caller to properly handle nil templates. +// +// This function is safe for concurrent access. +func (g *BgBlkTmplGenerator) CurrentTemplate() (*BlockTemplate, error) { + g.staleTemplateWg.Wait() + g.templateMtx.Lock() + template, err := g.template, g.templateErr + g.templateMtx.Unlock() + return template, err +} +// TemplateSubscription defines a subscription to receive block template updates +// from the background block template generator. The caller must call Stop on +// the subscription when it is no longer needed to free resources. +// +// NOTE: Notifications are dropped to make up for slow receivers to ensure +// notifications to other subscribers, as well as senders, are not blocked +// indefinitely. Since templates are typically only generated infrequently and +// receives must fall several templates behind before new ones are dropped, this +// should not affect callers in practice, however, if a caller wishes to +// guarantee that no templates are being dropped, they will need to ensure the +// channel is always processed quickly. +type TemplateSubscription struct { + g *BgBlkTmplGenerator + privC chan *BlockTemplate +} + +// C returns a channel that produces a stream of block templates as each new +// template is generated. Successive calls to C return the same channel. +// +// NOTE: Notifications are dropped to make up for slow receivers. See the +// template subscription type documentation for more details. +func (s *TemplateSubscription) C() <-chan *BlockTemplate { + return s.privC +} + +// Stop prevents any future template updates from being delivered and +// unsubscribes the associated subscription. +// +// NOTE: The channel is not closed to prevent a read from the channel succeeding +// incorrectly. +func (s *TemplateSubscription) Stop() { + s.g.subscriptionMtx.Lock() + delete(s.g.subscriptions, s) + s.g.subscriptionMtx.Unlock() +} + +// publishTemplate sends the provided template on the channel associated with +// the subscription. Nil templates are ignored. +func (s *TemplateSubscription) publishTemplate(template *BlockTemplate) { + if template == nil { return } - if !g.regenScheduler.Reset(dt) { - minrLog.Errorf("failed to reset template regen scheduler") + // Make use of a non-blocking send along with the buffered channel to allow + // notifications to be dropped to make up for slow receivers. + select { + case s.privC <- template: + default: } } -// cancelScheduledRegen terminates a scheduled template regeneration. -func (g *BgBlkTmplGenerator) cancelScheduledRegen() { - if g.regenScheduler != nil { - g.regenScheduler.Stop() - g.regenScheduler = nil - } -} - -// notifySubscribersHandler updates subscribers of newly created block -// templates. All subscribers are unsubscribed after being updated and required -// to resubscribe after a template update. It must be run as a goroutine. +// notifySubscribersHandler updates subscribers with newly created block +// templates. +// +// This must be run as a goroutine. func (g *BgBlkTmplGenerator) notifySubscribersHandler(ctx context.Context) { - minrLog.Debug("Starting notify subscribers handler") for { select { - case t := <-g.notifyChan: + case template := <-g.notifySubscribers: g.subscriptionMtx.Lock() - for c := range g.subscribers { - c <- t - close(c) - delete(g.subscribers, c) + for subscription := range g.subscriptions { + subscription.publishTemplate(template) } g.subscriptionMtx.Unlock() + case <-ctx.Done(): - minrLog.Debug("Notify subscribers handler done") g.wg.Done() return } } } -// RequestTemplateUpdate subscribes a client for a block template update. The -// client is unsubscribed after being updated and required to resubscribe -// for the next block template update. -func (g *BgBlkTmplGenerator) RequestTemplateUpdate() chan *BlockTemplate { - updateChan := make(chan *BlockTemplate, 1) +// Subscribe subscribes a client for block template updates. The returned +// template subscription contains functions to retrieve a channel that produces +// the stream of block templates and to stop the stream when the caller no +// longer wishes to receive new templates. +// +// The current template associated with the background block template generator, +// if any, is immediately sent to the returned subscription stream. +func (g *BgBlkTmplGenerator) Subscribe() *TemplateSubscription { + // Create the subscription with a buffered channel that is large enough to + // handle twice the number of templates that can be induced due votes in + // order to provide a reasonable amount of buffering before dropping + // notifications due to a slow receiver. + maxVoteInducedRegens := g.maxVotesPerBlock - g.minVotesRequired + 1 + c := make(chan *BlockTemplate, maxVoteInducedRegens*2) + subscription := &TemplateSubscription{ + g: g, + privC: c, + } g.subscriptionMtx.Lock() - g.subscribers[updateChan] = struct{}{} + g.subscriptions[subscription] = struct{}{} g.subscriptionMtx.Unlock() - return updateChan + + // Send existing valid template immediately. + template, err := g.CurrentTemplate() + if err == nil { + subscription.publishTemplate(template) + } + + return subscription } -// regenTemplate regenerates the block template. This must be run as a -// goroutine. -func (g *BgBlkTmplGenerator) regenTemplate(ctx context.Context) { - select { - case <-ctx.Done(): - return - default: - // Non-blocking receive fallthrough. - } - // Do not generate block templates if the chain is reorganizing. - g.chainReorgMtx.Lock() - if g.chainReorg { - g.chainReorgMtx.Unlock() - return - } - g.chainReorgMtx.Unlock() +// regenQueueHandler immediately forwards items from the regen event queue +// channel to the regen event messages channel when it would not block or adds +// the event to an internal queue to be processed as soon as the receiver +// becomes available. This ensures that queueing regen events never blocks +// despite how busy the regen handler might become during a burst of events. +// +// This must be run as a goroutine. +func (g *BgBlkTmplGenerator) regenQueueHandler(ctx context.Context) { + var q []regenEvent + var out, dequeue chan<- regenEvent = g.regenEventMsgs, nil + skipQueue := out + var next regenEvent + for { + select { + case n := <-g.queueRegenEvent: + // Either send to destination channel immediately when skipQueue is + // non-nil (queue is empty) and reader is ready, or append to the + // queue and send later. + select { + case skipQueue <- n: + default: + q = append(q, n) + dequeue = out + skipQueue = nil + next = q[0] + } - // Regenerate block templates on mainnet only when the chain is synced. - if !g.tg.chain.IsCurrent() && !g.permitConnectionlessMining { + case dequeue <- next: + copy(q, q[1:]) + q = q[:len(q)-1] + if len(q) == 0 { + dequeue = nil + skipQueue = out + } else { + next = q[0] + } + + case <-ctx.Done(): + g.wg.Done() + return + } + } +} + +// regenHandlerState houses the state used in the regen event handler goroutine. +// It is separated from the background template generator to ensure it is only +// available within the scope of the goroutine. +type regenHandlerState struct { + // isReorganizing indicates the chain is currently undergoing a + // reorganization and therefore the generator should not attempt to create + // new templates until the reorganization has completed. + isReorganizing bool + + // These fields are used to implement a periodic regeneration timeout that + // can be reset at any time without needing to create a new one and the + // associated extra garbage. + // + // regenTimer is a underlying timer that is used to implement the timeout. + // + // regenChanDrained indicates whether or not the channel for the regen timer + // has already been read and is used when resetting the timer to ensure the + // channel is drained when the timer is stopped as described in the timer + // documentation. + // + // lastGeneratedTime specifies the timestamp the current template was + // generated. + regenTimer *time.Timer + regenChanDrained bool + lastGeneratedTime int64 + + // These fields are used to control the various generation states when a new + // block that requires votes has been received. + // + // awaitingMinVotesHash is selectively set when a new tip block has been + // received that requires votes until the minimum number of required votes + // has been received. + // + // maxVotesTimeout is selectively enabled once the minimum number of + // required votes for the current tip block has been received and is + // disabled once the maximum number of votes has been received. This + // effectively sets a timeout to give the remaining votes an opportunity to + // propagate prior to forcing a template with less than the maximum number + // of votes. + awaitingMinVotesHash *chainhash.Hash + maxVotesTimeout <-chan time.Time + + // These fields are used to handle detection of side chain votes and + // potentially reorganizing the chain to a variant of the current tip when + // it is unable to obtain the minimum required votes. + // + // awaitingSideChainMinVotes houses the known blocks that build from the + // same parent as the current tip and will only be selectively populated + // when none of the current possible tips have the minimum number of + // required votes. + // + // trackSideChainsTimeout is selectively enabled when a new tip block has + // been received in order to give the minimum number of required votes + // needed to build a block template on it an opportunity to propagate before + // attempting to find any other variants that extend the same parent as the + // current tip with enough votes to force a reorganation. This ensures the + // first block that is seen is chosen to build templates on so long as it + // receives the mininum required votes in order to prevent PoW miners from + // being able to gain an advantage through vote withholding. It is disabled + // if the minimum number of votes is received prior to the timeout. + awaitingSideChainMinVotes map[chainhash.Hash]struct{} + trackSideChainsTimeout <-chan time.Time + + // failedGenRetryTimeout is selectively enabled in the rare case a template + // fails to generate so it can be regenerated again after a delay. A + // template should never fail to generate in practice, however, future code + // changes might break that assumption and thus it is important to handle + // the case properly. + failedGenRetryTimeout <-chan time.Time + + // These fields track the block and height that the next template to be + // generated will build on. This may not be the same as the current tip in + // the case it has not yet received the minimum number of required votes + // needed to build a template on it. + // + // baseBlockHash is the hash of the block the next template to be generated + // will build on. + // + // baseBlockHeight is the height of the block identified by the base block + // hash. + baseBlockHash chainhash.Hash + baseBlockHeight uint32 +} + +// makeRegenHandlerState returns a regen handler state that is ready to use. +func makeRegenHandlerState() regenHandlerState { + regenTimer := time.NewTimer(math.MaxInt64) + regenTimer.Stop() + return regenHandlerState{ + regenTimer: regenTimer, + regenChanDrained: true, + awaitingSideChainMinVotes: make(map[chainhash.Hash]struct{}), + } +} + +// stopRegenTimer stops the regen timer while ensuring to read from the timer's +// channel in the case the timer already expired which can happen due to the +// fact the stop happens in between channel reads. This behavior is well +// documented in the Timer docs. +// +// NOTE: This function must not be called concurrent with any other receives on +// the timer's channel. +func (state *regenHandlerState) stopRegenTimer() { + t := state.regenTimer + if !t.Stop() && !state.regenChanDrained { + <-t.C + } + state.regenChanDrained = true +} + +// resetRegenTimer resets the regen timer to the given duration while ensuring +// to read from the timer's channel in the case the timer already expired which +// can happen due to the fact the reset happens in between channel reads. This +// behavior is well documented in the Timer docs. +// +// NOTE: This function must not be called concurrent with any other receives on +// the timer's channel. +func (state *regenHandlerState) resetRegenTimer(d time.Duration) { + state.stopRegenTimer() + state.regenTimer.Reset(d) + state.regenChanDrained = false +} + +// clearSideChainTracking removes all tracking for minimum required votes on +// side chain blocks as well as clears the associated timeout that must +// transpire before said tracking is enabled. +func (state *regenHandlerState) clearSideChainTracking() { + for hash := range state.awaitingSideChainMinVotes { + delete(state.awaitingSideChainMinVotes, hash) + } + state.trackSideChainsTimeout = nil +} + +// genTemplateAsync cancels any asynchronous block template that is already +// currently being generated and launches a new goroutine to asynchronously +// generate a new one with the provided reason. It also handles updating the +// current template and error associated with the generator with the results in +// a concurrent safe fashion and, in the case a successful template is +// generated, notifies the subscription handler goroutine with the new template. +func (g *BgBlkTmplGenerator) genTemplateAsync(ctx context.Context, reason templateUpdateReason) { + // Cancel any other templates that might currently be in the process of + // being generated and create a new context that can be cancelled for the + // new template that is about to be generated. + g.cancelTemplateMtx.Lock() + g.cancelTemplate() + ctx, g.cancelTemplate = context.WithCancel(ctx) + g.cancelTemplateMtx.Unlock() + + // Ensure that attempts to retrieve the current template block until the + // new template is generated when it is because the parent has changed or + // new votes are available in order to avoid handing out a template that + // is guaranteed to be stale soon after. + blockRetrieval := reason == turNewParent || reason == turNewVotes + if blockRetrieval { + g.staleTemplateWg.Add(1) + } + go func(ctx context.Context, reason templateUpdateReason, blockRetrieval bool) { + if blockRetrieval { + defer g.staleTemplateWg.Done() + } + + // Pick a mining address at random and generate a block template that + // pays to it. + payToAddr := g.miningAddrs[g.prng.Intn(len(g.miningAddrs))] + template, err := g.tg.NewBlockTemplate(payToAddr) + // NOTE: err is handled below. + + // Don't update the state or notify subscribers when the template + // generation was cancelled. + if ctx.Err() != nil { + return + } + + // Update the current template state with the results and notify + // subscribed clients of the new template so long as it's valid. + g.setCurrentTemplate(template, err) + if err == nil && template != nil { + // Ensure the goroutine exits cleanly during shutdown. + select { + case <-ctx.Done(): + return + + case g.notifySubscribers <- template: + } + } + }(ctx, reason, blockRetrieval) +} + +// curTplHasNumVotes returns whether or not the current template is valid, +// builds on the provided hash, and contains the specified number of votes. +func (g *BgBlkTmplGenerator) curTplHasNumVotes(votedOnHash *chainhash.Hash, numVotes uint16) bool { + g.templateMtx.Lock() + template, err := g.template, g.templateErr + g.templateMtx.Unlock() + if template == nil || err != nil { + return false + } + if g.template.Block.Header.PrevBlock != *votedOnHash { + return false + } + return g.template.Block.Header.Voters == numVotes +} + +// numVotesForBlock returns the number of votes on the provided block hash that +// are known. +func (g *BgBlkTmplGenerator) numVotesForBlock(votedOnBlock *chainhash.Hash) uint16 { + return uint16(len(g.tg.txSource.VoteHashesForBlock(votedOnBlock))) +} + +// handleBlockConnected handles the rtBlockConnected event by either immediately +// generating a new template building on the block when it will still be prior +// to stake validation height or selectively setting up timeouts to give the +// votes a chance to propagate once the template will be at or after stake +// validation height. +// +// This function is only intended for use by the regen handler goroutine. +func (g *BgBlkTmplGenerator) handleBlockConnected(ctx context.Context, state *regenHandlerState, block *dcrutil.Block, chainTip *blockchain.BestState) { + // Clear all vote tracking when the current chain tip changes. + state.awaitingMinVotesHash = nil + state.clearSideChainTracking() + + // Nothing more to do if the connected block is not the current chain tip. + // This can happen in rare cases such as if more than one new block shows up + // while generating a template. Due to the requirement for votes later in + // the chain, it should almost never happen in practice once the chain has + // progressed that far, however, it is required for correctness. It is also + // worth noting that it happens more frequently earlier in the chain before + // voting starts, particularly in simulation networks with low difficulty. + blockHeight := block.MsgBlock().Header.Height + blockHash := block.Hash() + if int64(blockHeight) != chainTip.Height || *blockHash != chainTip.Hash { return } - // Pick a mining address at random. - rand.Seed(time.Now().UnixNano()) - payToAddr := g.miningAddrs[rand.Intn(len(g.miningAddrs))] + // Generate a new template immediately when it will be prior to stake + // validation height which means no votes are required. + newTemplateHeight := blockHeight + 1 + if newTemplateHeight < uint32(g.tg.chainParams.StakeValidationHeight) { + state.stopRegenTimer() + state.failedGenRetryTimeout = nil + state.baseBlockHash = *blockHash + state.baseBlockHeight = blockHeight + g.genTemplateAsync(ctx, turNewParent) + return + } - // Regenerate the block template. - template, err := g.tg.NewBlockTemplate(payToAddr) + // At this point the template will be at or after stake validation height, + // and therefore requires the inclusion of votes on the previous block to be + // valid. + + // Generate a new template immediately when the maximum number of votes + // for the block are already known. + numVotes := g.numVotesForBlock(blockHash) + if numVotes >= g.maxVotesPerBlock { + state.stopRegenTimer() + state.failedGenRetryTimeout = nil + state.baseBlockHash = *blockHash + state.baseBlockHeight = blockHeight + g.genTemplateAsync(ctx, turNewParent) + return + } + + // Update the state so the next template generated will build on the block + // and set a timeout to give the remaining votes an opportunity to propagate + // when the minimum number of required votes for the block are already + // known. This provides a balance between preferring to generate block + // templates with max votes and not waiting too long before starting work on + // the next block. + if numVotes >= g.minVotesRequired { + state.stopRegenTimer() + state.failedGenRetryTimeout = nil + state.baseBlockHash = *blockHash + state.baseBlockHeight = blockHeight + state.maxVotesTimeout = time.After(maxVoteTimeoutDuration) + return + } + + // Mark the state as waiting for the minimum number of required votes needed + // to build a template on the block to be received and set a timeout to give + // them an opportunity to propagate before attempting to find any other + // variants that extend the same parent with enough votes to force a + // reorganization. This ensures the first block that is seen is chosen to + // build templates on so long as it receives the mininum required votes in + // order to prevent PoW miners from being able to gain an advantage through + // vote withholding. + // + // Also, the regen timer for the current template is stopped since chances + // are high that the votes will be received and it is ideal to avoid + // regenerating a template that will likely be stale shortly. The regen + // timer is reset after the timeout if needed. + state.stopRegenTimer() + state.awaitingMinVotesHash = blockHash + state.trackSideChainsTimeout = time.After(minVotesTimeoutDuration) +} + +// handleBlockDisconnected handles the rtBlockDisconnected event by immediately +// generating a new template based on the new tip since votes for it are +// either necessarily already known due to being included in the block being +// disconnected or not required due to moving before stake validation height. +// +// This function is only intended for use by the regen handler goroutine. +func (g *BgBlkTmplGenerator) handleBlockDisconnected(ctx context.Context, state *regenHandlerState, block *dcrutil.Block, chainTip *blockchain.BestState) { + // Clear all vote tracking when the current chain tip changes. + state.awaitingMinVotesHash = nil + state.clearSideChainTracking() + + // Nothing more to do if the current chain tip is not the block prior to the + // block that was disconnected. This can happen in rare cases such as when + // forcing disconnects via block invalidation. In practice, disconnects + // happen as a result of chain reorganizations and thus this code will not + // be executed, however, it is required for correctness. + prevHeight := block.MsgBlock().Header.Height - 1 + prevHash := &block.MsgBlock().Header.PrevBlock + if int64(prevHeight) != chainTip.Height || *prevHash != chainTip.Hash { + return + } + + // NOTE: The block being disconnected necessarily has votes for the block + // that is becoming the new tip and they should ideally be extracted here to + // ensure they are available for use when building the template. However, + // the underlying template generator currently relies on pulling the votes + // out of the mempool and performs this task itself. In the future, the + // template generator should ideally accept the votes to include directly. + + // Generate a new template building on the new tip. + state.stopRegenTimer() + state.failedGenRetryTimeout = nil + state.baseBlockHash = *prevHash + state.baseBlockHeight = prevHeight + g.genTemplateAsync(ctx, turNewParent) +} + +// handleBlockAccepted handles the rtBlockAccepted event by establishing vote +// tracking for the block when it is a variant that extends the same parent as +// the current tip, the current tip does not have the minimum number of required +// votes, and the initial timeout to provide them an opportunity to propagate +// has already expired. +// +// This function is only intended for use by the regen handler goroutine. +func (g *BgBlkTmplGenerator) handleBlockAccepted(ctx context.Context, state *regenHandlerState, block *dcrutil.Block, chainTip *blockchain.BestState) { + // Ignore side chain blocks while still waiting for the side chain tracking + // timeout to expire. This provides a bias towards the first block that is + // seen in order to prevent PoW miners from being able to gain an advantage + // through vote withholding. + if state.trackSideChainsTimeout != nil { + return + } + + // Ignore side chain blocks when building on it would produce a block prior + // to stake validation height which means no votes are required and + // therefore no additional handling is necessary. + blockHeight := block.MsgBlock().Header.Height + newTemplateHeight := blockHeight + 1 + if newTemplateHeight < uint32(g.tg.chainParams.StakeValidationHeight) { + return + } + + // Ignore side chain blocks when the current tip already has enough votes + // for a template to be built on it. This ensures the first block that is + // seen is chosen to build templates on so long as it receives the mininum + // required votes in order to prevent PoW miners from being able to gain an + // advantage through vote withholding. + if state.awaitingMinVotesHash == nil { + return + } + + // Ignore blocks that are prior to the current tip. + if blockHeight < uint32(chainTip.Height) { + return + } + + // Ignore main chain tip block since it is handled by the connect path. + blockHash := block.Hash() + if *blockHash == chainTip.Hash { + return + } + + // Ignore side chain blocks when the current template is already building on + // the current tip or the accepted block is not a sibling of the current + // best chain tip. + alreadyBuildingOnCurTip := state.baseBlockHash == chainTip.Hash + acceptedPrevHash := &block.MsgBlock().Header.PrevBlock + if alreadyBuildingOnCurTip || *acceptedPrevHash != chainTip.PrevHash { + return + } + + // Setup tracking for votes on the block. + state.awaitingSideChainMinVotes[*blockHash] = struct{}{} +} + +// handleVote handles the rtVote event by determining if the vote is for a block +// the current state is monitoring and reacting accordingly. At a high level, +// this entails either establishing a timeout once the minimum number of +// required votes for the current tip have been received to provide the +// remaining votes an opportunity to propagate, regenerating the current +// template as a result of the vote, or potentially reorganizing the chain to a +// new tip that has enough votes in the case the current tip is unable to obtain +// the required votes. +// +// This function is only intended for use by the regen handler goroutine. +func (g *BgBlkTmplGenerator) handleVote(ctx context.Context, state *regenHandlerState, voteTx *dcrutil.Tx, chainTip *blockchain.BestState) { + votedOnHash, _ := stake.SSGenBlockVotedOn(voteTx.MsgTx()) + + // The awaiting min votes hash is selectively set once a block is connected + // such that a new template that builds on it will be at or after stake + // validation height until the minimum number of votes required to build a + // template are received. + // + // Update the state so the next template generated will build on the current + // tip once at least the minimum number of required votes for it has been + // received and either set a timeout to give the remaining votes an + // opportunity to propagate if the maximum number of votes is not already + // known or generate a new template immediately when they are. This + // provides a balance between preferring to generate block templates with + // max votes and not waiting too long before starting work on the next + // block. + minVotesHash := state.awaitingMinVotesHash + if minVotesHash != nil && votedOnHash == *minVotesHash { + numVotes := g.numVotesForBlock(minVotesHash) + minrLog.Debugf("Received vote %s for tip block %s (%d total)", + voteTx.Hash(), minVotesHash, numVotes) + if numVotes >= g.minVotesRequired { + // Ensure the next template generated builds on the tip and clear + // all vote tracking to lock the current current tip in now that it + // has the minimum required votes. + state.stopRegenTimer() + state.failedGenRetryTimeout = nil + state.baseBlockHash = *minVotesHash + state.baseBlockHeight = uint32(chainTip.Height) + state.awaitingMinVotesHash = nil + state.clearSideChainTracking() + + // Generate a new template immediately when the maximum number of + // votes for the block are already known. + if numVotes >= g.maxVotesPerBlock { + g.genTemplateAsync(ctx, turNewParent) + return + } + + // Set a timeout to give the remaining votes an opportunity to + // propagate. + state.maxVotesTimeout = time.After(maxVoteTimeoutDuration) + } + return + } + + // Generate a template on new votes for the block the current state is + // configured to build the next block template on when either the maximum + // number of votes is received for it or once the minimum number of required + // votes has been received and the propagation delay timeout that is started + // upon receipt of said minimum votes has expired. + // + // Note that the base block hash is only updated to the current tip once it + // has received the minimum number of required votes, so this will continue + // to detect votes for the parent of the current tip prior to the point the + // new tip has received enough votes. + // + // This ensures new templates that include the new votes are generated + // immediately upon receiving the maximum number of votes as well as any + // additional votes that arrive after the initial timeout. + if votedOnHash == state.baseBlockHash { + // Avoid regenerating the current template if it is already building on + // the expected block and already has the maximum number of votes. + if g.curTplHasNumVotes(&votedOnHash, g.maxVotesPerBlock) { + state.maxVotesTimeout = nil + return + } + + numVotes := g.numVotesForBlock(&votedOnHash) + minrLog.Debugf("Received vote %s for current template %s (%d total)", + voteTx.Hash(), votedOnHash, numVotes) + if numVotes >= g.maxVotesPerBlock || state.maxVotesTimeout == nil { + // The template needs to be updated due to a new parent the first + // time it is generated and due to new votes on subsequent votes. + // The max votes timeout is only non-nil before the first time it is + // generated. + tplUpdateReason := turNewVotes + if state.maxVotesTimeout != nil { + tplUpdateReason = turNewParent + } + + // Cancel the max votes timeout (if set). + state.maxVotesTimeout = nil + + state.stopRegenTimer() + state.failedGenRetryTimeout = nil + g.genTemplateAsync(ctx, tplUpdateReason) + return + } + } + + // Reorganize to an alternative chain tip when it receives at least the + // minimum required number of votes in the case the current chain tip does + // not receive the minimum number of required votes within an initial + // timeout period. + // + // Note that the potential side chain blocks to consider are only populated + // in the aforementioned case. + if _, ok := state.awaitingSideChainMinVotes[votedOnHash]; ok { + numVotes := g.numVotesForBlock(&votedOnHash) + minrLog.Debugf("Received vote %s for side chain block %s (%d total)", + voteTx.Hash(), votedOnHash, numVotes) + if numVotes >= g.minVotesRequired { + err := g.chain.ForceHeadReorganization(chainTip.Hash, votedOnHash) + if err != nil { + return + } + + // Prevent votes on other tip candidates from causing reorg again + // since the new chain tip has enough votes. + state.clearSideChainTracking() + return + } + } +} + +// handleTemplateUpdate handles the rtTemlateUpdate event by updating the state +// accordingly. +// +// This function is only intended for use by the regen handler goroutine. +func (g *BgBlkTmplGenerator) handleTemplateUpdate(state *regenHandlerState, tplUpdate templateUpdate) { + // Schedule a template regen if it failed to generate for some reason. This + // should be exceedingly rare in practice. + if tplUpdate.err != nil && state.failedGenRetryTimeout == nil { + state.failedGenRetryTimeout = time.After(time.Second) + return + } + if tplUpdate.template == nil { + return + } + + // Ensure the base block details match the template. + state.baseBlockHash = tplUpdate.template.Block.Header.PrevBlock + state.baseBlockHeight = tplUpdate.template.Block.Header.Height - 1 + + // Update the state related to template regeneration due to new regular + // transactions. + state.lastGeneratedTime = time.Now().Unix() + state.resetRegenTimer(templateRegenSecs * time.Second) +} + +// handleRegenEvent handles all regen events by determining the event reason and +// reacting accordingly. For example, it calls the appropriate associated event +// handler for the events that have one and prevents templates from being +// generating in the middle of reorgs. +// +// This function is only intended for use by the regen handler goroutine. +func (g *BgBlkTmplGenerator) handleRegenEvent(ctx context.Context, state *regenHandlerState, event regenEvent) { + // Handle chain reorg messages up front since all of the following logic + // only applies when not in the middle of reorganizing. + switch event.reason { + case rtReorgStarted: + // Ensure that attempts to retrieve the current template block until the + // new template after the reorg is generated. + g.staleTemplateWg.Add(1) + + // Mark the state as reorganizing. + state.isReorganizing = true + + // Stop all timeouts and clear all vote tracking. + state.stopRegenTimer() + state.failedGenRetryTimeout = nil + state.awaitingMinVotesHash = nil + state.maxVotesTimeout = nil + state.clearSideChainTracking() + + // Clear the current template and associated base block for the next + // generated template. + g.setCurrentTemplate(nil, nil) + state.baseBlockHash = zeroHash + state.baseBlockHeight = 0 + return + + case rtReorgDone: + state.isReorganizing = false + + // Treat the tip block as if it was just connected when a reorganize + // finishes so the existing code paths are run. + // + // An error should be impossible here since the request is for the block + // the chain believes is the current tip which means it must exist. + chainTip := g.chain.BestSnapshot() + tipBlock, err := g.chain.BlockByHash(&chainTip.Hash) + if err != nil { + g.setCurrentTemplate(nil, err) + } else { + g.handleBlockConnected(ctx, state, tipBlock, chainTip) + } + + g.staleTemplateWg.Done() + return + } + + // Do not generate block templates when the chain is in the middle of + // reorganizing. + if state.isReorganizing { + return + } + + // Do not generate block templates when the chain is not synced unless + // specifically requested to. + if !g.allowUnsyncedMining && !g.tg.blockManager.IsCurrent() { + return + } + + chainTip := g.chain.BestSnapshot() + switch event.reason { + case rtBlockConnected: + block := event.value.(*dcrutil.Block) + g.handleBlockConnected(ctx, state, block, chainTip) + + case rtBlockDisconnected: + block := event.value.(*dcrutil.Block) + g.handleBlockDisconnected(ctx, state, block, chainTip) + + case rtBlockAccepted: + block := event.value.(*dcrutil.Block) + g.handleBlockAccepted(ctx, state, block, chainTip) + + case rtVote: + voteTx := event.value.(*dcrutil.Tx) + g.handleVote(ctx, state, voteTx, chainTip) + + case rtTemplateUpdated: + tplUpdate := event.value.(templateUpdate) + g.handleTemplateUpdate(state, tplUpdate) + } +} + +// tipSiblingsSortedByVotes returns all blocks other than the current tip block +// that also extend its parent sorted by the number of votes each has in +// descending order. +func (g *BgBlkTmplGenerator) tipSiblingsSortedByVotes(state *regenHandlerState) []*blockWithNumVotes { + // Obtain all of the current blocks that extend the same parent as the + // current tip. The error is ignored here because it is deprecated. + generation, _ := g.chain.TipGeneration() + + // Nothing else to consider if there is only a single block which will be + // the current tip itself. + if len(generation) <= 1 { + return nil + } + + siblings := make([]*blockWithNumVotes, 0, len(generation)-1) + for i := range generation { + hash := &generation[i] + if *hash == *state.awaitingMinVotesHash { + continue + } + + numVotes := g.numVotesForBlock(hash) + siblings = append(siblings, &blockWithNumVotes{ + Hash: *hash, + NumVotes: numVotes, + }) + } + sort.Sort(sort.Reverse(byNumberOfVotes(siblings))) + return siblings +} + +// handleTrackSideChainsTimeout handles potentially reorganizing the chain to a +// side chain block with the most votes in the case the the minimum number of +// votes needed to build a block template on the current tip have not been +// received within a certain timeout. +// +// It also doubles to reset the regen timer for the current template in the case +// no validate candidates are found since it is disabled when setting up this +// timeout to prevent creating new templates that would very likely be stale +// soon after. +// +// This function is only intended for use by the regen handler goroutine. +func (g *BgBlkTmplGenerator) handleTrackSideChainsTimeout(ctx context.Context, state *regenHandlerState) { + // Don't allow side chain variants to override the current tip when it + // already has the minimum required votes. + if state.awaitingMinVotesHash == nil { + return + } + + // Reorganize the chain to a valid sibling of the current tip that has at + // least the minimum number of required votes while preferring the most + // votes. + // + // Also, while looping, add each tip the map of side chain blocks to monitor + // for votes in the event there are not currently any eligible candidates + // since they may become eligible as votes arrive. + sortedSiblings := g.tipSiblingsSortedByVotes(state) + for _, sibling := range sortedSiblings { + if sibling.NumVotes >= g.minVotesRequired { + err := g.chain.ForceHeadReorganization(*state.awaitingMinVotesHash, + sibling.Hash) + if err != nil { + // Try the next block in the case of failure to reorg. + continue + } + + // Prevent votes on other tip candidates from causing reorg again + // since the new chain tip has enough votes. The reorg event clears + // the state, but, since there is a backing queue for the events, + // and the reorg itself might haven taken a bit of time, it could + // allow new side chain blocks or votes on existing ones in before + // the reorg events are processed. Thus, update the state to + // indicate the next template is to be built on the new tip to + // prevent any possible logic races. + state.awaitingMinVotesHash = nil + state.clearSideChainTracking() + state.stopRegenTimer() + state.failedGenRetryTimeout = nil + state.baseBlockHash = sibling.Hash + return + } + + state.awaitingSideChainMinVotes[sibling.Hash] = struct{}{} + } + + // Generate a new template building on the parent of the current tip when + // there is not already an existing template and the initial timeout has + // elapsed upon receiving the new tip without receiving votes for it. There + // will typically only not be an existing template when the generator is + // first instantiated and after a chain reorganization. + if state.baseBlockHash == zeroHash { + chainTip := g.chain.BestSnapshot() + state.failedGenRetryTimeout = nil + state.baseBlockHash = chainTip.PrevHash + state.baseBlockHeight = uint32(chainTip.Height - 1) + g.genTemplateAsync(ctx, turNewParent) + return + } + + // At this point, no viable candidates to change the current template were + // found, so reset the regen timer for the current template. + state.resetRegenTimer(templateRegenSecs * time.Second) +} + +// regenHandler is the main workhorse for generating new templates in response +// to regen events and also handles generating a new template during initial +// startup. +// +// This must be run as a goroutine. +func (g *BgBlkTmplGenerator) regenHandler(ctx context.Context) { + // Treat the tip block as if it was just connected when starting up so the + // existing code paths are run. + tipBlock, err := g.chain.BlockByHash(&g.chain.BestSnapshot().Hash) if err != nil { - minrLog.Debugf("block template generation failed: %v", err) - return + g.setCurrentTemplate(nil, err) + } else { + g.queueRegenEvent <- regenEvent{rtBlockConnected, tipBlock} } - msgBlock := *template.Block - - // In order to efficiently store the variations of block templates that - // have been provided to callers, save a pointer to the block keyed by - // the merkle root + timestamp. This along with the data that is - // included in a work submission is used to rebuild the block before - // checking the submitted solution. - g.templatePoolMtx.Lock() - g.templatePool[templateKey(&msgBlock.Header)] = &msgBlock - g.templatePoolMtx.Unlock() - - t := *template - g.notifyChan <- &t - - g.templateMtx.Lock() - g.currentTemplate = template - g.templateMtx.Unlock() - - // Update the last template regen time. - atomic.StoreInt64(&g.lastRegen, time.Now().Unix()) -} - -// pruneOldBlockTemplates prunes all block templates with heights lower than -// the height of the best block's parent from the template pool. -func (g *BgBlkTmplGenerator) pruneOldBlockTemplates(bestHeight int64) { - g.templatePoolMtx.Lock() - for key, block := range g.templatePool { - height := int64(block.Header.Height) - if height < bestHeight-1 { - delete(g.templatePool, key) - } - } - g.templatePoolMtx.Unlock() -} - -// updateParentTemplate updates the parent template with the the current -// block template. This must be called when a new block has been connected. -func (g *BgBlkTmplGenerator) updateParentTemplate() { - stakeValidationHeight := g.tg.chainParams.StakeValidationHeight - g.templateMtx.Lock() - if g.currentTemplate != nil { - height := g.currentTemplate.Height - - // Set the parent template if the chain is matured to SVH-1. - if height >= stakeValidationHeight-2 { - g.parentTemplate = g.currentTemplate - minrLog.Debugf("Parent template updated to height: %v", - g.parentTemplate.Height) - } - g.currentTemplate = nil - - // Prune invalidated block templates. - g.pruneOldBlockTemplates(height) - } - g.templateMtx.Unlock() -} - -// handleChainReorgStarted updates the chain reorg state of the background -// template generator when a chain reorganization commences. -func (g *BgBlkTmplGenerator) handleChainReorgStarted() { - g.chainReorgMtx.Lock() - g.chainReorg = true - g.chainReorgMtx.Unlock() -} - -// handleChainReorgDone updates the chain reorg state of the background -// template generator and immdiately regenerates the block template -// when a chain reorganization concludes. -func (g *BgBlkTmplGenerator) handleChainReorgDone(ctx context.Context) { - g.chainReorgMtx.Lock() - g.chainReorg = false - g.chainReorgMtx.Unlock() - - // Regenerate block template. - go g.regenTemplate(ctx) -} - -// handleDisconnectedBlock empties the template pool and updates the cached -// templates based on the disconnected block's height. -func (g *BgBlkTmplGenerator) handleDisconnectedBlock(discHeight int64) { - // Remove all block templates in the template pool. - g.templatePoolMtx.Lock() - for key := range g.templatePool { - delete(g.templatePool, key) - } - g.templatePoolMtx.Unlock() - - g.templateMtx.Lock() - // Unset the current template. - g.currentTemplate = nil - - // Unset the parent template if it shares the same height as the - // disconnected block. - if g.parentTemplate != nil && g.parentTemplate.Height == discHeight { - g.parentTemplate = nil - } - g.templateMtx.Unlock() -} - -// handleConnectedBlock updates the parent template and generates a new block -// template if the connected block height is below SVH-1. -func (g *BgBlkTmplGenerator) handleConnectedBlock(ctx context.Context, connHeight int64) { - // Update the cached parent template. - g.updateParentTemplate() - - // Generate a new block template if the connected block height - // is below SVH-1. - if connHeight <= g.tg.chainParams.StakeValidationHeight-2 { - go g.regenTemplate(ctx) - } -} - -// onVoteReceivedHandler triggers block template regeneration based on -// votes received by the mempool. This must be run as a goroutine. -func (g *BgBlkTmplGenerator) onVoteReceivedHandler(ctx context.Context) { - minrLog.Debug("Starting vote received handler") + state := makeRegenHandlerState() for { select { - case voteTx := <-g.voteChan: - // Fetch the block height and hash of the block being voted on by - // the new vote received. - votedOnHash, votedOnHeight := stake.SSGenBlockVotedOn(voteTx) - best := g.tg.chain.BestSnapshot() + case event := <-g.regenEventMsgs: + g.handleRegenEvent(ctx, &state, event) - if votedOnHash.IsEqual(&best.Hash) && - votedOnHeight == uint32(best.Height) { - currTipVotes := - len(g.tg.txSource.VoteHashesForBlock(&best.Hash)) + // This timeout is selectively enabled once the minimum number of + // required votes has been received in order to give the remaining votes + // an opportunity to propagate. It is disabled if the remaining votes + // are received prior to the timeout. + case <-state.maxVotesTimeout: + state.maxVotesTimeout = nil + g.genTemplateAsync(ctx, turNewParent) - // Regenerate the block template immediately if the block being - // voted on has the maximum number of votes possible. - if currTipVotes == int(g.tg.chainParams.TicketsPerBlock) { - g.cancelScheduledRegen() - go g.regenTemplate(ctx) - } + // This timeout is selectively enabled when a new block is connected in + // order to give the minimum number of required votes needed to build a + // block template on it an opportunity to propagate before attempting to + // find any other variants that extend the same parent as the current + // tip with enough votes to force a reorganization. This ensures the + // first block that is seen is chosen to build templates on so long as + // it receives the mininum required votes in order to prevent PoW miners + // from being able to gain an advantage through vote withholding. It is + // disabled if the minimum number of votes is received prior to the + // timeout. + case <-state.trackSideChainsTimeout: + state.trackSideChainsTimeout = nil + g.handleTrackSideChainsTimeout(ctx, &state) - // Schedule a block template regeneration if the new vote received - // is voting on the current chain tip with at least the minimum - // required number of votes by the network but less than the - // maximum number of votes possible for a block. - if currTipVotes < int(g.tg.chainParams.TicketsPerBlock) && - currTipVotes >= int((g.tg.chainParams.TicketsPerBlock/2)+1) { - g.scheduleRegen(ctx, time.Second) - } + // This timeout is selectively enabled once a template has been + // generated in order to allow the template to be periodically + // regenerated with new transactions. Note that votes have special + // handling as described above. + case <-state.regenTimer.C: + // Mark the timer's channel as having been drained so the timer can + // safely be reset. + state.regenChanDrained = true + + // Generate a new template when there are new transactions + // available. + if g.tg.txSource.LastUpdated().Unix() > state.lastGeneratedTime { + state.failedGenRetryTimeout = nil + g.genTemplateAsync(ctx, turNewTxns) + continue } - if !votedOnHash.IsEqual(&best.Hash) && - votedOnHeight != uint32(best.Height) { - currTipVotes := - len(g.tg.txSource.VoteHashesForBlock(&best.Hash)) - votedOnVotes := - len(g.tg.txSource.VoteHashesForBlock(&votedOnHash)) - if votedOnVotes > currTipVotes { - // Regenerate the block template immediately if the block being - // voted on has the maximum number of votes possible. - if votedOnVotes == int(g.tg.chainParams.TicketsPerBlock) { - g.cancelScheduledRegen() - go g.regenTemplate(ctx) - } + // There are no new transactions to include and the initial timeout + // has been triggered, so reset the timer to check again in one + // second. + state.resetRegenTimer(time.Second) - // Schdule a block template if the new vote received is voting - // on a side chain tip with at least the minimum required number - // of votes by the network and more votes than the current chain - // tip. - if votedOnVotes < int(g.tg.chainParams.TicketsPerBlock) && - votedOnVotes >= int((g.tg.chainParams.TicketsPerBlock/2)+1) { - g.scheduleRegen(ctx, time.Second) - } - } - } + // This timeout is selectively enabled in the rare case a template fails + // to generate and disabled prior to attempts at generating a new one. + case <-state.failedGenRetryTimeout: + state.failedGenRetryTimeout = nil + g.genTemplateAsync(ctx, turNewParent) case <-ctx.Done(): - minrLog.Debug("Vote received handler done") g.wg.Done() return } } } -// OnVoteReceived signals a new vote received by the mempool. -func (g *BgBlkTmplGenerator) OnVoteReceived(tx *wire.MsgTx) { - go func() { - g.voteChan <- tx - }() +// ChainReorgStarted informs the background block template generator that a +// chain reorganization has started. It is caller's responsibility to ensure +// this is only invoked as described. +func (g *BgBlkTmplGenerator) ChainReorgStarted() { + g.queueRegenEvent <- regenEvent{rtReorgStarted, nil} } -// generator generates new templates based on mempool activity for vote -// and non-vote transactions and the time elapsed since the last template -// regeneration. This must be run as a goroutine. -func (g *BgBlkTmplGenerator) generator(ctx context.Context) { - minrLog.Trace("Starting background block template generator") - var isTicking bool - ticker := time.NewTicker(time.Millisecond * 500) - defer ticker.Stop() +// ChainReorgDone informs the background block template generator that a chain +// reorganization has completed. It is caller's responsibility to ensure this +// is only invoked as described. +func (g *BgBlkTmplGenerator) ChainReorgDone() { + g.queueRegenEvent <- regenEvent{rtReorgDone, nil} +} - // Immediately generate a block template if the chain is below SVH-1. - if g.tg.chain.BestSnapshot().Height <= - g.tg.chainParams.StakeValidationHeight-2 { - go g.regenTemplate(ctx) - } +// BlockAccepted informs the background block template generator that a block +// has been accepted to the block chain. It is caller's responsibility to +// ensure this is only invoked as described. +// +// This function is safe for concurrent access. +func (g *BgBlkTmplGenerator) BlockAccepted(block *dcrutil.Block) { + g.queueRegenEvent <- regenEvent{rtBlockAccepted, block} +} - for { - select { - case <-ticker.C: - lastRegen := atomic.LoadInt64(&g.lastRegen) - lastUpdated := g.tg.txSource.LastUpdated().Unix() - now := time.Now().Unix() - sinceLastRegen := now - lastRegen +// BlockConnected informs the background block template generator that a block +// has been connected to the main chain. It is caller's responsibility to +// ensure this is only invoked as described. +// +// This function is safe for concurrent access. +func (g *BgBlkTmplGenerator) BlockConnected(block *dcrutil.Block) { + g.queueRegenEvent <- regenEvent{rtBlockConnected, block} +} - if !isTicking { - atomic.StoreInt64(&g.lastRegen, now) - sinceLastRegen = 0 - isTicking = true - } +// BlockDisconnected informs the background block template generator that a +// block has been disconnected from the main chain. It is caller's +// responsibility to ensure this is only invoked as described. +// +// This function is safe for concurrent access. +func (g *BgBlkTmplGenerator) BlockDisconnected(block *dcrutil.Block) { + g.queueRegenEvent <- regenEvent{rtBlockDisconnected, block} +} - // Regenerate the block template if the mempool was updated after - // last template regeneration and 30 seconds has elapsed or 60 - // seconds has elapsed since the last template regeneration. - if (lastUpdated > lastRegen && sinceLastRegen > - templateRegenSecs) || sinceLastRegen > maxTemplateRegenSecs { - go g.regenTemplate(ctx) - } - - case <-ctx.Done(): - minrLog.Debug("Background block template generator done") - g.wg.Done() - return - } - } +// VoteReceived informs the background block template generator that a new vote +// has been received. It is the caller's reponsibility to ensure this is only +// invoked with valid votes. +// +// This function is safe for concurrent access. +func (g *BgBlkTmplGenerator) VoteReceived(tx *dcrutil.Tx) { + g.queueRegenEvent <- regenEvent{rtVote, tx} } // Run starts the background block template generator and all other goroutines @@ -2418,8 +3312,8 @@ func (g *BgBlkTmplGenerator) generator(ctx context.Context) { // is cancelled. func (g *BgBlkTmplGenerator) Run(ctx context.Context) { g.wg.Add(3) - go g.generator(ctx) + go g.regenQueueHandler(ctx) + go g.regenHandler(ctx) go g.notifySubscribersHandler(ctx) - go g.onVoteReceivedHandler(ctx) g.wg.Wait() } diff --git a/rpctest/rpc_harness_test.go b/rpctest/rpc_harness_test.go index 673fef66..b34d627a 100644 --- a/rpctest/rpc_harness_test.go +++ b/rpctest/rpc_harness_test.go @@ -628,8 +628,11 @@ func TestHarness(t *testing.T) { nodeInfo.Blocks, expectedChainHeight) } - for _, testCase := range harnessTestCases { - testCase(mainHarness, t) + // Skip tests when running with -short + if !testing.Short() { + for _, testCase := range harnessTestCases { + testCase(mainHarness, t) + } } testTearDownAll(t) diff --git a/rpctest/votingwallet_test.go b/rpctest/votingwallet_test.go index 3de202b2..952f14a2 100644 --- a/rpctest/votingwallet_test.go +++ b/rpctest/votingwallet_test.go @@ -54,6 +54,11 @@ func testCanPassSVH(t *testing.T, vw *VotingWallet) { } func TestMinimalVotingWallet(t *testing.T) { + // Skip tests when running with -short + if testing.Short() { + t.Skip("Skipping minimal voting wallet in short mode") + } + var handlers *rpcclient.NotificationHandlers net := &chaincfg.SimNetParams diff --git a/server.go b/server.go index 7d7c8b59..e404c70d 100644 --- a/server.go +++ b/server.go @@ -2574,9 +2574,9 @@ func newServer(listenAddrs []string, db database.DB, chainParams *chaincfg.Param ExistsAddrIndex: s.existsAddrIndex, AddTxToFeeEstimation: s.feeEstimator.AddMemPoolTransaction, RemoveTxFromFeeEstimation: s.feeEstimator.RemoveMemPoolTransaction, - OnVoteReceived: func(voteTx *wire.MsgTx) { + OnVoteReceived: func(voteTx *dcrutil.Tx) { if s.bg != nil { - s.bg.OnVoteReceived(voteTx) + s.bg.VoteReceived(voteTx) } }, }