45627c7a6a
Co-authored-by: Brannon King <countprimes@gmail.com>
642 lines
20 KiB
Go
642 lines
20 KiB
Go
// Copyright (c) 2014-2016 The btcsuite developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package cpuminer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/lbryio/lbcd/blockchain"
|
|
"github.com/lbryio/lbcd/chaincfg"
|
|
"github.com/lbryio/lbcd/chaincfg/chainhash"
|
|
"github.com/lbryio/lbcd/mining"
|
|
"github.com/lbryio/lbcd/wire"
|
|
btcutil "github.com/lbryio/lbcutil"
|
|
)
|
|
|
|
const (
|
|
// maxNonce is the maximum value a nonce can be in a block header.
|
|
maxNonce = ^uint32(0) // 2^32 - 1
|
|
|
|
// maxExtraNonce is the maximum value an extra nonce used in a coinbase
|
|
// transaction can be.
|
|
maxExtraNonce = ^uint64(0) // 2^64 - 1
|
|
|
|
// hpsUpdateSecs is the number of seconds to wait in between each
|
|
// update to the hashes per second monitor.
|
|
hpsUpdateSecs = 10
|
|
|
|
// hashUpdateSec is the number of seconds each worker waits in between
|
|
// notifying the speed monitor with how many hashes have been completed
|
|
// while they are actively searching for a solution. This is done to
|
|
// reduce the amount of syncs between the workers that must be done to
|
|
// keep track of the hashes per second.
|
|
hashUpdateSecs = 15
|
|
)
|
|
|
|
var (
|
|
// defaultNumWorkers is the default number of workers to use for mining
|
|
// and is based on the number of processor cores. This helps ensure the
|
|
// system stays reasonably responsive under heavy load.
|
|
defaultNumWorkers = uint32(runtime.NumCPU())
|
|
)
|
|
|
|
// Config is a descriptor containing the cpu miner configuration.
|
|
type Config struct {
|
|
// ChainParams identifies which chain parameters the cpu miner is
|
|
// associated with.
|
|
ChainParams *chaincfg.Params
|
|
|
|
// BlockTemplateGenerator identifies the instance to use in order to
|
|
// generate block templates that the miner will attempt to solve.
|
|
BlockTemplateGenerator *mining.BlkTmplGenerator
|
|
|
|
// MiningAddrs is a list of payment addresses to use for the generated
|
|
// blocks. Each generated block will randomly choose one of them.
|
|
MiningAddrs []btcutil.Address
|
|
|
|
// ProcessBlock defines the function to call with any solved blocks.
|
|
// It typically must run the provided block through the same set of
|
|
// rules and handling as any other block coming from the network.
|
|
ProcessBlock func(*btcutil.Block, blockchain.BehaviorFlags) (bool, error)
|
|
|
|
// ConnectedCount defines the function to use to obtain how many other
|
|
// peers the server is connected to. This is used by the automatic
|
|
// persistent mining routine to determine whether or it should attempt
|
|
// mining. This is useful because there is no point in mining when not
|
|
// connected to any peers since there would no be anyone to send any
|
|
// found blocks to.
|
|
ConnectedCount func() int32
|
|
|
|
// IsCurrent defines the function to use to obtain whether or not the
|
|
// block chain is current. This is used by the automatic persistent
|
|
// mining routine to determine whether or it should attempt mining.
|
|
// This is useful because there is no point in mining if the chain is
|
|
// not current since any solved blocks would be on a side chain and and
|
|
// up orphaned anyways.
|
|
IsCurrent func() bool
|
|
}
|
|
|
|
// CPUMiner provides facilities for solving blocks (mining) using the CPU in
|
|
// a concurrency-safe manner. It consists of two main goroutines -- a speed
|
|
// monitor and a controller for worker goroutines which generate and solve
|
|
// blocks. The number of goroutines can be set via the SetMaxGoRoutines
|
|
// function, but the default is based on the number of processor cores in the
|
|
// system which is typically sufficient.
|
|
type CPUMiner struct {
|
|
sync.Mutex
|
|
g *mining.BlkTmplGenerator
|
|
cfg Config
|
|
numWorkers uint32
|
|
started bool
|
|
discreteMining bool
|
|
submitBlockLock sync.Mutex
|
|
wg sync.WaitGroup
|
|
workerWg sync.WaitGroup
|
|
updateNumWorkers chan struct{}
|
|
queryHashesPerSec chan float64
|
|
updateHashes chan uint64
|
|
speedMonitorQuit chan struct{}
|
|
quit chan struct{}
|
|
}
|
|
|
|
// speedMonitor handles tracking the number of hashes per second the mining
|
|
// process is performing. It must be run as a goroutine.
|
|
func (m *CPUMiner) speedMonitor() {
|
|
log.Tracef("CPU miner speed monitor started")
|
|
|
|
var hashesPerSec float64
|
|
var totalHashes uint64
|
|
ticker := time.NewTicker(time.Second * hpsUpdateSecs)
|
|
defer ticker.Stop()
|
|
|
|
out:
|
|
for {
|
|
select {
|
|
// Periodic updates from the workers with how many hashes they
|
|
// have performed.
|
|
case numHashes := <-m.updateHashes:
|
|
totalHashes += numHashes
|
|
|
|
// Time to update the hashes per second.
|
|
case <-ticker.C:
|
|
curHashesPerSec := float64(totalHashes) / hpsUpdateSecs
|
|
if hashesPerSec == 0 {
|
|
hashesPerSec = curHashesPerSec
|
|
}
|
|
hashesPerSec = (hashesPerSec + curHashesPerSec) / 2
|
|
totalHashes = 0
|
|
if hashesPerSec != 0 {
|
|
log.Debugf("Hash speed: %6.0f kilohashes/s",
|
|
hashesPerSec/1000)
|
|
}
|
|
|
|
// Request for the number of hashes per second.
|
|
case m.queryHashesPerSec <- hashesPerSec:
|
|
// Nothing to do.
|
|
|
|
case <-m.speedMonitorQuit:
|
|
break out
|
|
}
|
|
}
|
|
|
|
m.wg.Done()
|
|
log.Tracef("CPU miner speed monitor done")
|
|
}
|
|
|
|
// submitBlock submits the passed block to network after ensuring it passes all
|
|
// of the consensus validation rules.
|
|
func (m *CPUMiner) submitBlock(block *btcutil.Block) bool {
|
|
m.submitBlockLock.Lock()
|
|
defer m.submitBlockLock.Unlock()
|
|
|
|
// Ensure the block is not stale since a new block could have shown up
|
|
// while the solution was being found. Typically that condition is
|
|
// detected and all work on the stale block is halted to start work on
|
|
// a new block, but the check only happens periodically, so it is
|
|
// possible a block was found and submitted in between.
|
|
msgBlock := block.MsgBlock()
|
|
if !msgBlock.Header.PrevBlock.IsEqual(&m.g.BestSnapshot().Hash) {
|
|
log.Debugf("Block submitted via CPU miner with previous "+
|
|
"block %s is stale", msgBlock.Header.PrevBlock)
|
|
return false
|
|
}
|
|
|
|
// Process this block using the same rules as blocks coming from other
|
|
// nodes. This will in turn relay it to the network like normal.
|
|
isOrphan, err := m.cfg.ProcessBlock(block, blockchain.BFNone)
|
|
if err != nil {
|
|
// Anything other than a rule violation is an unexpected error,
|
|
// so log that error as an internal error.
|
|
if _, ok := err.(blockchain.RuleError); !ok {
|
|
log.Errorf("Unexpected error while processing "+
|
|
"block submitted via CPU miner: %v", err)
|
|
return false
|
|
}
|
|
|
|
log.Debugf("Block submitted via CPU miner rejected: %v", err)
|
|
return false
|
|
}
|
|
if isOrphan {
|
|
log.Debugf("Block submitted via CPU miner is an orphan")
|
|
return false
|
|
}
|
|
|
|
// The block was accepted.
|
|
coinbaseTx := block.MsgBlock().Transactions[0].TxOut[0]
|
|
log.Infof("Block submitted via CPU miner accepted (hash %s, "+
|
|
"amount %v)", block.Hash(), btcutil.Amount(coinbaseTx.Value))
|
|
return true
|
|
}
|
|
|
|
// solveBlock attempts to find some combination of a nonce, extra nonce, and
|
|
// current timestamp which makes the passed block hash to a value less than the
|
|
// target difficulty. The timestamp is updated periodically and the passed
|
|
// block is modified with all tweaks during this process. This means that
|
|
// when the function returns true, the block is ready for submission.
|
|
//
|
|
// This function will return early with false when conditions that trigger a
|
|
// stale block such as a new block showing up or periodically when there are
|
|
// new transactions and enough time has elapsed without finding a solution.
|
|
func (m *CPUMiner) solveBlock(msgBlock *wire.MsgBlock, blockHeight int32,
|
|
ticker *time.Ticker, quit chan struct{}) bool {
|
|
|
|
// Choose a random extra nonce offset for this block template and
|
|
// worker.
|
|
enOffset, err := wire.RandomUint64()
|
|
if err != nil {
|
|
log.Errorf("Unexpected error while generating random "+
|
|
"extra nonce offset: %v", err)
|
|
enOffset = 0
|
|
}
|
|
|
|
// Create some convenience variables.
|
|
header := &msgBlock.Header
|
|
targetDifficulty := blockchain.CompactToBig(header.Bits)
|
|
|
|
// Initial state.
|
|
lastGenerated := time.Now()
|
|
lastTxUpdate := m.g.TxSource().LastUpdated()
|
|
hashesCompleted := uint64(0)
|
|
|
|
// Note that the entire extra nonce range is iterated and the offset is
|
|
// added relying on the fact that overflow will wrap around 0 as
|
|
// provided by the Go spec.
|
|
for extraNonce := uint64(0); extraNonce < maxExtraNonce; extraNonce++ {
|
|
// Update the extra nonce in the block template with the
|
|
// new value by regenerating the coinbase script and
|
|
// setting the merkle root to the new value.
|
|
m.g.UpdateExtraNonce(msgBlock, blockHeight, extraNonce+enOffset)
|
|
|
|
// Search through the entire nonce range for a solution while
|
|
// periodically checking for early quit and stale block
|
|
// conditions along with updates to the speed monitor.
|
|
for i := uint32(0); i <= maxNonce; i++ {
|
|
select {
|
|
case <-quit:
|
|
return false
|
|
|
|
case <-ticker.C:
|
|
m.updateHashes <- hashesCompleted
|
|
hashesCompleted = 0
|
|
|
|
// The current block is stale if the best block
|
|
// has changed.
|
|
best := m.g.BestSnapshot()
|
|
if !header.PrevBlock.IsEqual(&best.Hash) {
|
|
return false
|
|
}
|
|
|
|
// The current block is stale if the memory pool
|
|
// has been updated since the block template was
|
|
// generated and it has been at least one
|
|
// minute.
|
|
if lastTxUpdate != m.g.TxSource().LastUpdated() &&
|
|
time.Now().After(lastGenerated.Add(time.Minute)) {
|
|
|
|
return false
|
|
}
|
|
|
|
m.g.UpdateBlockTime(msgBlock)
|
|
|
|
default:
|
|
// Non-blocking select to fall through
|
|
}
|
|
|
|
// Update the nonce and hash the block header. Each
|
|
// hash is actually a double sha256 (two hashes), so
|
|
// increment the number of hashes completed for each
|
|
// attempt accordingly.
|
|
header.Nonce = i
|
|
hash := header.BlockHash()
|
|
hashesCompleted += 2
|
|
|
|
// The block is solved when the new block hash is less
|
|
// than the target difficulty. Yay!
|
|
if blockchain.HashToBig(&hash).Cmp(targetDifficulty) <= 0 {
|
|
m.updateHashes <- hashesCompleted
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// generateBlocks is a worker that is controlled by the miningWorkerController.
|
|
// It is self contained in that it creates block templates and attempts to solve
|
|
// them while detecting when it is performing stale work and reacting
|
|
// accordingly by generating a new block template. When a block is solved, it
|
|
// is submitted.
|
|
//
|
|
// It must be run as a goroutine.
|
|
func (m *CPUMiner) generateBlocks(quit chan struct{}) {
|
|
log.Tracef("Starting generate blocks worker")
|
|
|
|
// Start a ticker which is used to signal checks for stale work and
|
|
// updates to the speed monitor.
|
|
ticker := time.NewTicker(time.Second * hashUpdateSecs)
|
|
defer ticker.Stop()
|
|
out:
|
|
for {
|
|
// Quit when the miner is stopped.
|
|
select {
|
|
case <-quit:
|
|
break out
|
|
default:
|
|
// Non-blocking select to fall through
|
|
}
|
|
|
|
// Wait until there is a connection to at least one other peer
|
|
// since there is no way to relay a found block or receive
|
|
// transactions to work on when there are no connected peers.
|
|
if m.cfg.ConnectedCount() == 0 {
|
|
time.Sleep(time.Second)
|
|
continue
|
|
}
|
|
|
|
// No point in searching for a solution before the chain is
|
|
// synced. Also, grab the same lock as used for block
|
|
// submission, since the current block will be changing and
|
|
// this would otherwise end up building a new block template on
|
|
// a block that is in the process of becoming stale.
|
|
m.submitBlockLock.Lock()
|
|
curHeight := m.g.BestSnapshot().Height
|
|
if curHeight != 0 && !m.cfg.IsCurrent() {
|
|
m.submitBlockLock.Unlock()
|
|
time.Sleep(time.Second)
|
|
continue
|
|
}
|
|
|
|
// Choose a payment address at random.
|
|
rand.Seed(time.Now().UnixNano())
|
|
payToAddr := m.cfg.MiningAddrs[rand.Intn(len(m.cfg.MiningAddrs))]
|
|
|
|
// Create a new block template using the available transactions
|
|
// in the memory pool as a source of transactions to potentially
|
|
// include in the block.
|
|
template, err := m.g.NewBlockTemplate(payToAddr)
|
|
m.submitBlockLock.Unlock()
|
|
if err != nil {
|
|
errStr := fmt.Sprintf("Failed to create new block "+
|
|
"template: %v", err)
|
|
log.Errorf(errStr)
|
|
continue
|
|
}
|
|
|
|
// Attempt to solve the block. The function will exit early
|
|
// with false when conditions that trigger a stale block, so
|
|
// a new block template can be generated. When the return is
|
|
// true a solution was found, so submit the solved block.
|
|
if m.solveBlock(template.Block, curHeight+1, ticker, quit) {
|
|
block := btcutil.NewBlock(template.Block)
|
|
m.submitBlock(block)
|
|
}
|
|
}
|
|
|
|
m.workerWg.Done()
|
|
log.Tracef("Generate blocks worker done")
|
|
}
|
|
|
|
// miningWorkerController launches the worker goroutines that are used to
|
|
// generate block templates and solve them. It also provides the ability to
|
|
// dynamically adjust the number of running worker goroutines.
|
|
//
|
|
// It must be run as a goroutine.
|
|
func (m *CPUMiner) miningWorkerController() {
|
|
// launchWorkers groups common code to launch a specified number of
|
|
// workers for generating blocks.
|
|
var runningWorkers []chan struct{}
|
|
launchWorkers := func(numWorkers uint32) {
|
|
for i := uint32(0); i < numWorkers; i++ {
|
|
quit := make(chan struct{})
|
|
runningWorkers = append(runningWorkers, quit)
|
|
|
|
m.workerWg.Add(1)
|
|
go m.generateBlocks(quit)
|
|
}
|
|
}
|
|
|
|
// Launch the current number of workers by default.
|
|
runningWorkers = make([]chan struct{}, 0, m.numWorkers)
|
|
launchWorkers(m.numWorkers)
|
|
|
|
out:
|
|
for {
|
|
select {
|
|
// Update the number of running workers.
|
|
case <-m.updateNumWorkers:
|
|
// No change.
|
|
numRunning := uint32(len(runningWorkers))
|
|
if m.numWorkers == numRunning {
|
|
continue
|
|
}
|
|
|
|
// Add new workers.
|
|
if m.numWorkers > numRunning {
|
|
launchWorkers(m.numWorkers - numRunning)
|
|
continue
|
|
}
|
|
|
|
// Signal the most recently created goroutines to exit.
|
|
for i := numRunning - 1; i >= m.numWorkers; i-- {
|
|
close(runningWorkers[i])
|
|
runningWorkers[i] = nil
|
|
runningWorkers = runningWorkers[:i]
|
|
}
|
|
|
|
case <-m.quit:
|
|
for _, quit := range runningWorkers {
|
|
close(quit)
|
|
}
|
|
break out
|
|
}
|
|
}
|
|
|
|
// Wait until all workers shut down to stop the speed monitor since
|
|
// they rely on being able to send updates to it.
|
|
m.workerWg.Wait()
|
|
close(m.speedMonitorQuit)
|
|
m.wg.Done()
|
|
}
|
|
|
|
// Start begins the CPU mining process as well as the speed monitor used to
|
|
// track hashing metrics. Calling this function when the CPU miner has
|
|
// already been started will have no effect.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (m *CPUMiner) Start() {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
// Nothing to do if the miner is already running or if running in
|
|
// discrete mode (using GenerateNBlocks).
|
|
if m.started || m.discreteMining {
|
|
return
|
|
}
|
|
|
|
m.quit = make(chan struct{})
|
|
m.speedMonitorQuit = make(chan struct{})
|
|
m.wg.Add(2)
|
|
go m.speedMonitor()
|
|
go m.miningWorkerController()
|
|
|
|
m.started = true
|
|
log.Infof("CPU miner started")
|
|
}
|
|
|
|
// Stop gracefully stops the mining process by signalling all workers, and the
|
|
// speed monitor to quit. Calling this function when the CPU miner has not
|
|
// already been started will have no effect.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (m *CPUMiner) Stop() {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
// Nothing to do if the miner is not currently running or if running in
|
|
// discrete mode (using GenerateNBlocks).
|
|
if !m.started || m.discreteMining {
|
|
return
|
|
}
|
|
|
|
close(m.quit)
|
|
m.wg.Wait()
|
|
m.started = false
|
|
log.Infof("CPU miner stopped")
|
|
}
|
|
|
|
// IsMining returns whether or not the CPU miner has been started and is
|
|
// therefore currenting mining.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (m *CPUMiner) IsMining() bool {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
return m.started
|
|
}
|
|
|
|
// HashesPerSecond returns the number of hashes per second the mining process
|
|
// is performing. 0 is returned if the miner is not currently running.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (m *CPUMiner) HashesPerSecond() float64 {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
// Nothing to do if the miner is not currently running.
|
|
if !m.started {
|
|
return 0
|
|
}
|
|
|
|
return <-m.queryHashesPerSec
|
|
}
|
|
|
|
// SetNumWorkers sets the number of workers to create which solve blocks. Any
|
|
// negative values will cause a default number of workers to be used which is
|
|
// based on the number of processor cores in the system. A value of 0 will
|
|
// cause all CPU mining to be stopped.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (m *CPUMiner) SetNumWorkers(numWorkers int32) {
|
|
if numWorkers == 0 {
|
|
m.Stop()
|
|
}
|
|
|
|
// Don't lock until after the first check since Stop does its own
|
|
// locking.
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
// Use default if provided value is negative.
|
|
if numWorkers < 0 {
|
|
m.numWorkers = defaultNumWorkers
|
|
} else {
|
|
m.numWorkers = uint32(numWorkers)
|
|
}
|
|
|
|
// When the miner is already running, notify the controller about the
|
|
// the change.
|
|
if m.started {
|
|
m.updateNumWorkers <- struct{}{}
|
|
}
|
|
}
|
|
|
|
// NumWorkers returns the number of workers which are running to solve blocks.
|
|
//
|
|
// This function is safe for concurrent access.
|
|
func (m *CPUMiner) NumWorkers() int32 {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
return int32(m.numWorkers)
|
|
}
|
|
|
|
// GenerateNBlocks generates the requested number of blocks. It is self
|
|
// contained in that it creates block templates and attempts to solve them while
|
|
// detecting when it is performing stale work and reacting accordingly by
|
|
// generating a new block template. When a block is solved, it is submitted.
|
|
// The function returns a list of the hashes of generated blocks.
|
|
func (m *CPUMiner) GenerateNBlocks(n uint32) ([]*chainhash.Hash, error) {
|
|
m.Lock()
|
|
|
|
// Respond with an error if server is already mining.
|
|
if m.started || m.discreteMining {
|
|
m.Unlock()
|
|
return nil, errors.New("Server is already CPU mining. Please call " +
|
|
"`setgenerate 0` before calling discrete `generate` commands.")
|
|
}
|
|
|
|
m.started = true
|
|
m.discreteMining = true
|
|
|
|
m.speedMonitorQuit = make(chan struct{})
|
|
m.wg.Add(1)
|
|
go m.speedMonitor()
|
|
|
|
m.Unlock()
|
|
|
|
log.Tracef("Generating %d blocks", n)
|
|
|
|
i := uint32(0)
|
|
blockHashes := make([]*chainhash.Hash, n)
|
|
|
|
// Start a ticker which is used to signal checks for stale work and
|
|
// updates to the speed monitor.
|
|
ticker := time.NewTicker(time.Second * hashUpdateSecs)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
// Read updateNumWorkers in case someone tries a `setgenerate` while
|
|
// we're generating. We can ignore it as the `generate` RPC call only
|
|
// uses 1 worker.
|
|
select {
|
|
case <-m.updateNumWorkers:
|
|
default:
|
|
}
|
|
|
|
// Grab the lock used for block submission, since the current block will
|
|
// be changing and this would otherwise end up building a new block
|
|
// template on a block that is in the process of becoming stale.
|
|
m.submitBlockLock.Lock()
|
|
curHeight := m.g.BestSnapshot().Height
|
|
|
|
// Choose a payment address at random.
|
|
rand.Seed(time.Now().UnixNano())
|
|
payToAddr := m.cfg.MiningAddrs[rand.Intn(len(m.cfg.MiningAddrs))]
|
|
|
|
// Create a new block template using the available transactions
|
|
// in the memory pool as a source of transactions to potentially
|
|
// include in the block.
|
|
template, err := m.g.NewBlockTemplate(payToAddr)
|
|
m.submitBlockLock.Unlock()
|
|
if err != nil {
|
|
errStr := fmt.Sprintf("Failed to create new block "+
|
|
"template: %v", err)
|
|
log.Errorf(errStr)
|
|
continue
|
|
}
|
|
|
|
// Attempt to solve the block. The function will exit early
|
|
// with false when conditions that trigger a stale block, so
|
|
// a new block template can be generated. When the return is
|
|
// true a solution was found, so submit the solved block.
|
|
if m.solveBlock(template.Block, curHeight+1, ticker, nil) {
|
|
block := btcutil.NewBlock(template.Block)
|
|
m.submitBlock(block)
|
|
blockHashes[i] = block.Hash()
|
|
i++
|
|
if i == n {
|
|
log.Tracef("Generated %d blocks", i)
|
|
m.Lock()
|
|
close(m.speedMonitorQuit)
|
|
m.wg.Wait()
|
|
m.started = false
|
|
m.discreteMining = false
|
|
m.Unlock()
|
|
return blockHashes, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// New returns a new instance of a CPU miner for the provided configuration.
|
|
// Use Start to begin the mining process. See the documentation for CPUMiner
|
|
// type for more details.
|
|
func New(cfg *Config) *CPUMiner {
|
|
return &CPUMiner{
|
|
g: cfg.BlockTemplateGenerator,
|
|
cfg: *cfg,
|
|
numWorkers: defaultNumWorkers,
|
|
updateNumWorkers: make(chan struct{}),
|
|
queryHashesPerSec: make(chan float64),
|
|
updateHashes: make(chan uint64),
|
|
}
|
|
}
|