mining: Overhaul background template generator.

Currently, block templates that provide work to PoW miners are generated
in response to them calling the getwork RPC.  However, unlike in a pure
PoW system, in Decred, when a new block is connected, a new block
template extending that block can't be generated until the minimum
number of required votes for it has been received.  This poses a
challenge for PoW miners because they are typically unaware of when
votes are received.  Consequently, miners poll getwork waiting until
they see a new template generated when a new block is connected.

Since only a minimum of 3 votes is required to build a template and new
templates are only generated in response to the miners calling getwork,
as just described, miners often, unfortunately, end up receiving a
template that only has 3 votes and begin working on it.  Worse, since
many new miners are not well versed in the intricacies of Decred voting,
they often aren't aware they really need to continue polling for new
votes and immediately switch over to the new template with more votes in
it in order to avoid receiving a reduced subsidy.  This often results in
new miners producing blocks with 3 votes.

Moreover, blocks with less than all 5 votes are not only undesirable for
the PoW miners because they receive reduced subsidy, they're also
undesirable for PoS stakers since it means the votes that were not
included don't receive the reward despite having voted simply due to a
propagation delay.

Another issue with the existing approach is that since it takes a bit of
time to generate and validate templates, particularly as the number of
transactions they include rises, miners periodically requesting updated
work to include the latest transactions have to wait for the template to
be built when they call getwork once the existing cached template is
expired.  This can result in undesirable delays for them.

In order to address the need to wait for templates to be built in that
case, there was recently some preliminary work that implemented a
background template generator which asynchronously generates templates
in response to events with the intention of allowing access to the
latest one without said random delays in the future.

However, that preliminary work did not address the issues around vote
propagation such as the tendency for miners to end up with the first
block template only containing the 3 fastest-propagating votes, nor did
it have robust handling for all potential corner cases and error
conditions.

Consequently, this overhauls the background block template generator to
add support for intelligent vote propagation handling, handle chain
reorganization to alternative blocks in the case the current tip is
unable to obtain enough votes, provide a subscription for a stream of
template updates, handle template generation errors, consider the
synchronization state as a part of determining if the chain is current,
track the reason for template updates, block current template retrieval
during operations that are in the process of making it stale, and
correct several corner cases.

It should be noted that this only implements the infrastructure and does
not switch getwork or the CPU miner over to use the background templates
yet.  This will be done in future commits.

The following is a high-level overview of the semantics this implements:

- Generate new templates immediately when prior to stake validation
  height since no votes are required
- Do not generate templates for intermediate blocks during a chain
  reorganization
- Do not generate templates before the chain is considered synchronized
- Prefer to create templates with maximum votes through the use of a
  timeout once the minimum votes have been received to provide the votes
  an opportunity to propagate with a fallback for immediate generation
  as soon as all votes are received
  - In the case the timeout expires and a template is created with less
    than the maximum number of votes, generate a new template
    immediately upon receiving more votes for the block it extends
- In the event there are competing blocks at the current tip, prefer to
  build a template on the first seen block so long as it receives the
  minimum number of required votes within a few seconds
- Generate new templates immediately when a block is disconnected to
  support future planned chain invalidation capabilities
- Generate new templates periodically when there are new regular transactions
  to include
- 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)

The following is a high-level overview of the changes:

- Initialize PRNG once instead of at every invocation
- Implement an asynchronous queue for all events to ensure normal chain
  and vote processing is not blocked
- Provide ability to subscribe for all template updates over a single
  channel to ensure none can indadvertently be missed as is possible with
  the current design
  - Always deliver existing template upon registration
  - Drop templates if the receiver falls too far behind in the same way
    tickers work
- Introduce tracking the reason for template updates so that it can
  ultimately be presented to miners to allow them to make better
  decisions about when to force their workers to switch
- Consider the current network sync state when checking if the chain is
  current
- Introduce a regen handler state such as timeouts, blocks to monitor
  for votes, and the block the next template should extend
- Add logic for selectively reacting to votes on the block the current
  template is extending, the current tip, and alternate competing blocks
  at the current tip using the aforementioned semantics
- Perform all template generation in separate goroutines with a
  cancellable context
  - Cancel any in progress templates that are being generated whenever
    generating a new one
- Introduce blocking current template retrieval when a new template that
  would cause the existing one to become stale is being generated
- Modify periodic regen handling to use a resettable timer for better
  efficiency and more fine grained control
- Remove template pool as that is something that should be handled by
  the code that is actually handing the templates out
- Rename and export the event notification funcs to make it clear
  they're not internal functions and also make it easier to eventually
  move the code into the mining package
- Expand and improve comments
- Store and return template generation errors
  - Schedule retry in the case of failure
- Correct several cases that were not being handled correctly and some
  undesirable behaviors such as block disconnects (as opposed to
  reorgs), reorganization to side chains, and notifying with stale
  templates
This commit is contained in:
Dave Collins 2019-05-19 09:20:03 -05:00
parent 773db5b8db
commit 826d66bb56
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
6 changed files with 1245 additions and 336 deletions

View File

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

View File

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

1538
mining.go

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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

View File

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