2015-02-04 13:52:59 +00:00
|
|
|
package miner
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"math/big"
|
|
|
|
"sort"
|
2015-02-15 15:16:27 +00:00
|
|
|
"sync"
|
2015-03-26 16:45:03 +00:00
|
|
|
"sync/atomic"
|
2015-02-28 22:09:49 +00:00
|
|
|
"time"
|
2015-02-04 13:52:59 +00:00
|
|
|
|
2015-03-18 12:00:01 +00:00
|
|
|
"github.com/ethereum/go-ethereum/common"
|
2015-02-04 13:52:59 +00:00
|
|
|
"github.com/ethereum/go-ethereum/core"
|
2015-03-23 17:27:05 +00:00
|
|
|
"github.com/ethereum/go-ethereum/core/state"
|
2015-02-04 13:52:59 +00:00
|
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
|
|
"github.com/ethereum/go-ethereum/event"
|
2015-03-01 15:09:59 +00:00
|
|
|
"github.com/ethereum/go-ethereum/logger"
|
2015-04-04 21:04:19 +00:00
|
|
|
"github.com/ethereum/go-ethereum/logger/glog"
|
2015-02-04 13:52:59 +00:00
|
|
|
"github.com/ethereum/go-ethereum/pow"
|
|
|
|
"gopkg.in/fatih/set.v0"
|
|
|
|
)
|
|
|
|
|
2015-03-01 15:09:59 +00:00
|
|
|
var jsonlogger = logger.NewJsonLogger()
|
|
|
|
|
2015-02-04 13:52:59 +00:00
|
|
|
type environment struct {
|
|
|
|
totalUsedGas *big.Int
|
|
|
|
state *state.StateDB
|
|
|
|
coinbase *state.StateObject
|
|
|
|
block *types.Block
|
2015-03-23 11:12:49 +00:00
|
|
|
family *set.Set
|
2015-02-04 13:52:59 +00:00
|
|
|
uncles *set.Set
|
|
|
|
}
|
|
|
|
|
2015-02-17 11:24:51 +00:00
|
|
|
func env(block *types.Block, eth core.Backend) *environment {
|
2015-03-06 17:26:16 +00:00
|
|
|
state := state.New(block.Root(), eth.StateDb())
|
2015-02-04 13:52:59 +00:00
|
|
|
env := &environment{
|
|
|
|
totalUsedGas: new(big.Int),
|
|
|
|
state: state,
|
|
|
|
block: block,
|
2015-03-23 11:12:49 +00:00
|
|
|
family: set.New(),
|
2015-02-04 13:52:59 +00:00
|
|
|
uncles: set.New(),
|
|
|
|
coinbase: state.GetOrNewStateObject(block.Coinbase()),
|
|
|
|
}
|
|
|
|
|
|
|
|
return env
|
|
|
|
}
|
|
|
|
|
2015-02-13 16:23:09 +00:00
|
|
|
type Work struct {
|
2015-02-28 19:58:37 +00:00
|
|
|
Number uint64
|
2015-03-03 20:04:31 +00:00
|
|
|
Nonce uint64
|
2015-02-28 19:58:37 +00:00
|
|
|
MixDigest []byte
|
|
|
|
SeedHash []byte
|
2015-02-13 16:23:09 +00:00
|
|
|
}
|
|
|
|
|
2015-02-04 23:05:47 +00:00
|
|
|
type Agent interface {
|
2015-02-09 15:20:34 +00:00
|
|
|
Work() chan<- *types.Block
|
2015-03-24 09:34:06 +00:00
|
|
|
SetReturnCh(chan<- *types.Block)
|
2015-02-09 15:20:34 +00:00
|
|
|
Stop()
|
2015-02-14 15:52:14 +00:00
|
|
|
Start()
|
2015-03-20 16:42:09 +00:00
|
|
|
GetHashRate() int64
|
2015-02-04 23:05:47 +00:00
|
|
|
}
|
|
|
|
|
2015-02-04 13:52:59 +00:00
|
|
|
type worker struct {
|
2015-03-26 16:45:03 +00:00
|
|
|
mu sync.Mutex
|
|
|
|
|
2015-02-09 15:20:34 +00:00
|
|
|
agents []Agent
|
2015-03-24 09:34:06 +00:00
|
|
|
recv chan *types.Block
|
2015-02-04 13:52:59 +00:00
|
|
|
mux *event.TypeMux
|
|
|
|
quit chan struct{}
|
|
|
|
pow pow.PoW
|
2015-03-26 16:45:03 +00:00
|
|
|
atWork int64
|
2015-02-04 13:52:59 +00:00
|
|
|
|
2015-04-05 16:57:03 +00:00
|
|
|
eth core.Backend
|
|
|
|
chain *core.ChainManager
|
|
|
|
proc *core.BlockProcessor
|
|
|
|
|
2015-03-18 12:00:01 +00:00
|
|
|
coinbase common.Address
|
2015-04-05 16:57:03 +00:00
|
|
|
extra []byte
|
2015-02-04 13:52:59 +00:00
|
|
|
|
2015-04-07 10:32:55 +00:00
|
|
|
currentMu sync.Mutex
|
|
|
|
current *environment
|
2015-02-13 16:23:09 +00:00
|
|
|
|
2015-03-23 11:12:49 +00:00
|
|
|
uncleMu sync.Mutex
|
|
|
|
possibleUncles map[common.Hash]*types.Block
|
|
|
|
|
2015-04-07 10:32:55 +00:00
|
|
|
txQueueMu sync.Mutex
|
|
|
|
txQueue map[common.Hash]*types.Transaction
|
|
|
|
|
|
|
|
mining int64
|
2015-02-04 13:52:59 +00:00
|
|
|
}
|
|
|
|
|
2015-03-18 12:00:01 +00:00
|
|
|
func newWorker(coinbase common.Address, eth core.Backend) *worker {
|
2015-04-07 10:32:55 +00:00
|
|
|
worker := &worker{
|
2015-03-23 11:12:49 +00:00
|
|
|
eth: eth,
|
|
|
|
mux: eth.EventMux(),
|
2015-03-24 09:34:06 +00:00
|
|
|
recv: make(chan *types.Block),
|
2015-03-23 11:12:49 +00:00
|
|
|
chain: eth.ChainManager(),
|
|
|
|
proc: eth.BlockProcessor(),
|
|
|
|
possibleUncles: make(map[common.Hash]*types.Block),
|
|
|
|
coinbase: coinbase,
|
2015-04-07 10:32:55 +00:00
|
|
|
txQueue: make(map[common.Hash]*types.Transaction),
|
2015-04-07 22:30:23 +00:00
|
|
|
quit: make(chan struct{}),
|
2015-02-09 15:20:34 +00:00
|
|
|
}
|
2015-04-07 10:32:55 +00:00
|
|
|
go worker.update()
|
|
|
|
go worker.wait()
|
|
|
|
|
|
|
|
worker.commitNewWork()
|
|
|
|
|
|
|
|
return worker
|
2015-02-09 15:20:34 +00:00
|
|
|
}
|
|
|
|
|
2015-04-07 10:32:55 +00:00
|
|
|
func (self *worker) pendingState() *state.StateDB {
|
|
|
|
self.currentMu.Lock()
|
|
|
|
defer self.currentMu.Unlock()
|
|
|
|
|
|
|
|
return self.current.state
|
|
|
|
}
|
2015-02-13 16:23:09 +00:00
|
|
|
|
2015-04-07 10:32:55 +00:00
|
|
|
func (self *worker) pendingBlock() *types.Block {
|
|
|
|
self.currentMu.Lock()
|
|
|
|
defer self.currentMu.Unlock()
|
2015-02-13 16:23:09 +00:00
|
|
|
|
2015-04-07 10:32:55 +00:00
|
|
|
return self.current.block
|
|
|
|
}
|
|
|
|
|
|
|
|
func (self *worker) start() {
|
2015-02-14 15:52:14 +00:00
|
|
|
// spin up agents
|
|
|
|
for _, agent := range self.agents {
|
|
|
|
agent.Start()
|
|
|
|
}
|
|
|
|
|
2015-04-07 10:32:55 +00:00
|
|
|
atomic.StoreInt64(&self.mining, 1)
|
2015-02-09 15:20:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (self *worker) stop() {
|
2015-04-07 22:30:23 +00:00
|
|
|
if atomic.LoadInt64(&self.mining) == 1 {
|
|
|
|
// stop all agents
|
|
|
|
for _, agent := range self.agents {
|
|
|
|
agent.Stop()
|
|
|
|
}
|
|
|
|
}
|
2015-02-13 16:23:09 +00:00
|
|
|
|
2015-04-07 22:30:23 +00:00
|
|
|
atomic.StoreInt64(&self.mining, 0)
|
2015-04-07 10:32:55 +00:00
|
|
|
atomic.StoreInt64(&self.atWork, 0)
|
2015-02-09 15:20:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (self *worker) register(agent Agent) {
|
2015-02-04 13:52:59 +00:00
|
|
|
self.agents = append(self.agents, agent)
|
2015-03-24 09:34:06 +00:00
|
|
|
agent.SetReturnCh(self.recv)
|
2015-02-04 13:52:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (self *worker) update() {
|
2015-04-07 10:32:55 +00:00
|
|
|
events := self.mux.Subscribe(core.ChainHeadEvent{}, core.ChainSideEvent{}, core.TxPreEvent{})
|
2015-02-04 13:52:59 +00:00
|
|
|
|
2015-02-28 22:09:49 +00:00
|
|
|
timer := time.NewTicker(2 * time.Second)
|
|
|
|
|
2015-02-04 13:52:59 +00:00
|
|
|
out:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case event := <-events.Chan():
|
2015-02-19 21:33:22 +00:00
|
|
|
switch ev := event.(type) {
|
2015-03-06 14:50:44 +00:00
|
|
|
case core.ChainHeadEvent:
|
2015-02-14 15:52:14 +00:00
|
|
|
self.commitNewWork()
|
2015-03-23 15:14:33 +00:00
|
|
|
case core.ChainSideEvent:
|
2015-03-23 11:12:49 +00:00
|
|
|
self.uncleMu.Lock()
|
|
|
|
self.possibleUncles[ev.Block.Hash()] = ev.Block
|
|
|
|
self.uncleMu.Unlock()
|
2015-04-07 10:32:55 +00:00
|
|
|
case core.TxPreEvent:
|
|
|
|
if atomic.LoadInt64(&self.mining) == 0 {
|
|
|
|
self.commitNewWork()
|
|
|
|
}
|
2015-02-04 13:52:59 +00:00
|
|
|
}
|
|
|
|
case <-self.quit:
|
|
|
|
break out
|
2015-02-28 22:09:49 +00:00
|
|
|
case <-timer.C:
|
2015-04-13 20:58:53 +00:00
|
|
|
if glog.V(logger.Detail) && atomic.LoadInt64(&self.mining) == 1 {
|
2015-04-04 21:04:19 +00:00
|
|
|
glog.Infoln("Hash rate:", self.HashRate(), "Khash")
|
|
|
|
}
|
2015-03-26 16:45:03 +00:00
|
|
|
|
|
|
|
// XXX In case all mined a possible uncle
|
2015-04-07 10:32:55 +00:00
|
|
|
if atomic.LoadInt64(&self.atWork) == 0 && atomic.LoadInt64(&self.mining) == 1 {
|
2015-03-26 16:45:03 +00:00
|
|
|
self.commitNewWork()
|
|
|
|
}
|
2015-02-04 13:52:59 +00:00
|
|
|
}
|
|
|
|
}
|
2015-02-13 16:23:09 +00:00
|
|
|
|
|
|
|
events.Unsubscribe()
|
2015-02-04 13:52:59 +00:00
|
|
|
}
|
|
|
|
|
2015-02-09 15:20:34 +00:00
|
|
|
func (self *worker) wait() {
|
|
|
|
for {
|
2015-03-24 09:34:06 +00:00
|
|
|
for block := range self.recv {
|
2015-03-26 16:45:03 +00:00
|
|
|
atomic.AddInt64(&self.atWork, -1)
|
|
|
|
|
|
|
|
if block == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2015-03-24 09:34:06 +00:00
|
|
|
if err := self.chain.InsertChain(types.Blocks{block}); err == nil {
|
|
|
|
for _, uncle := range block.Uncles() {
|
|
|
|
delete(self.possibleUncles, uncle.Hash())
|
2015-02-14 15:52:14 +00:00
|
|
|
}
|
2015-03-24 09:34:06 +00:00
|
|
|
self.mux.Post(core.NewMinedBlockEvent{block})
|
2015-03-25 12:51:12 +00:00
|
|
|
|
2015-04-04 21:04:19 +00:00
|
|
|
glog.V(logger.Info).Infof("🔨 Mined block #%v", block.Number())
|
2015-04-02 10:58:17 +00:00
|
|
|
|
2015-03-25 12:51:12 +00:00
|
|
|
jsonlogger.LogJson(&logger.EthMinerNewBlock{
|
|
|
|
BlockHash: block.Hash().Hex(),
|
|
|
|
BlockNumber: block.Number(),
|
|
|
|
ChainHeadHash: block.ParentHeaderHash.Hex(),
|
|
|
|
BlockPrevHash: block.ParentHeaderHash.Hex(),
|
|
|
|
})
|
2015-03-24 09:34:06 +00:00
|
|
|
} else {
|
|
|
|
self.commitNewWork()
|
2015-02-13 16:23:09 +00:00
|
|
|
}
|
2015-02-09 15:20:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (self *worker) push() {
|
2015-04-07 10:32:55 +00:00
|
|
|
if atomic.LoadInt64(&self.mining) == 1 {
|
2015-02-14 15:52:14 +00:00
|
|
|
self.current.block.Header().GasUsed = self.current.totalUsedGas
|
2015-02-13 16:23:09 +00:00
|
|
|
self.current.block.SetRoot(self.current.state.Root())
|
2015-02-04 13:52:59 +00:00
|
|
|
|
2015-02-14 15:52:14 +00:00
|
|
|
// push new work to agents
|
2015-02-13 16:23:09 +00:00
|
|
|
for _, agent := range self.agents {
|
2015-03-26 16:45:03 +00:00
|
|
|
atomic.AddInt64(&self.atWork, 1)
|
|
|
|
|
2015-03-24 09:34:06 +00:00
|
|
|
agent.Work() <- self.current.block.Copy()
|
2015-02-13 16:23:09 +00:00
|
|
|
}
|
2015-02-04 13:52:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-07 10:32:55 +00:00
|
|
|
func (self *worker) makeCurrent() {
|
2015-03-05 08:14:58 +00:00
|
|
|
block := self.chain.NewBlock(self.coinbase)
|
2015-04-04 11:27:17 +00:00
|
|
|
if block.Time() == self.chain.CurrentBlock().Time() {
|
|
|
|
block.Header().Time++
|
|
|
|
}
|
2015-04-05 16:57:03 +00:00
|
|
|
block.Header().Extra = self.extra
|
2015-03-05 08:14:58 +00:00
|
|
|
|
|
|
|
self.current = env(block, self.eth)
|
2015-03-23 11:12:49 +00:00
|
|
|
for _, ancestor := range self.chain.GetAncestors(block, 7) {
|
|
|
|
self.current.family.Add(ancestor.Hash())
|
|
|
|
}
|
|
|
|
|
2015-02-04 13:52:59 +00:00
|
|
|
parent := self.chain.GetBlock(self.current.block.ParentHash())
|
|
|
|
self.current.coinbase.SetGasPool(core.CalcGasLimit(parent, self.current.block))
|
2015-04-07 10:32:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (self *worker) commitNewWork() {
|
|
|
|
self.mu.Lock()
|
|
|
|
defer self.mu.Unlock()
|
|
|
|
self.uncleMu.Lock()
|
|
|
|
defer self.uncleMu.Unlock()
|
|
|
|
self.currentMu.Lock()
|
|
|
|
defer self.currentMu.Unlock()
|
|
|
|
|
|
|
|
self.makeCurrent()
|
2015-02-04 13:52:59 +00:00
|
|
|
|
|
|
|
transactions := self.eth.TxPool().GetTransactions()
|
|
|
|
sort.Sort(types.TxByNonce{transactions})
|
|
|
|
|
|
|
|
// Keep track of transactions which return errors so they can be removed
|
2015-03-23 15:35:44 +00:00
|
|
|
var (
|
2015-04-07 22:30:23 +00:00
|
|
|
remove = set.New()
|
2015-03-23 15:35:44 +00:00
|
|
|
tcount = 0
|
|
|
|
)
|
2015-04-13 20:58:53 +00:00
|
|
|
//gasLimit:
|
|
|
|
for _, tx := range transactions {
|
2015-04-08 15:14:58 +00:00
|
|
|
self.current.state.StartRecord(tx.Hash(), common.Hash{}, 0)
|
|
|
|
|
2015-02-04 13:52:59 +00:00
|
|
|
err := self.commitTransaction(tx)
|
|
|
|
switch {
|
2015-04-07 22:30:23 +00:00
|
|
|
case core.IsNonceErr(err) || core.IsInvalidTxErr(err):
|
2015-02-15 15:16:27 +00:00
|
|
|
// Remove invalid transactions
|
2015-03-18 12:00:01 +00:00
|
|
|
from, _ := tx.From()
|
|
|
|
self.chain.TxState().RemoveNonce(from, tx.Nonce())
|
2015-04-07 22:30:23 +00:00
|
|
|
remove.Add(tx.Hash())
|
2015-04-04 21:04:19 +00:00
|
|
|
|
2015-04-13 20:58:53 +00:00
|
|
|
if glog.V(logger.Detail) {
|
2015-04-04 21:04:19 +00:00
|
|
|
glog.Infof("TX (%x) failed, will be removed: %v\n", tx.Hash().Bytes()[:4], err)
|
2015-04-13 20:58:53 +00:00
|
|
|
//glog.Infoln(tx)
|
2015-04-04 21:04:19 +00:00
|
|
|
}
|
2015-02-19 21:33:22 +00:00
|
|
|
case state.IsGasLimitErr(err):
|
2015-04-13 20:58:53 +00:00
|
|
|
//glog.V(logger.Debug).Infof("Gas limit reached for block. %d TXs included in this block\n", i)
|
|
|
|
//break gasLimit
|
2015-03-23 15:35:44 +00:00
|
|
|
default:
|
|
|
|
tcount++
|
2015-02-04 13:52:59 +00:00
|
|
|
}
|
|
|
|
}
|
2015-04-08 18:47:32 +00:00
|
|
|
//self.eth.TxPool().InvalidateSet(remove)
|
2015-02-04 13:52:59 +00:00
|
|
|
|
2015-03-23 17:27:05 +00:00
|
|
|
var (
|
|
|
|
uncles []*types.Header
|
|
|
|
badUncles []common.Hash
|
|
|
|
)
|
2015-03-23 11:12:49 +00:00
|
|
|
for hash, uncle := range self.possibleUncles {
|
2015-03-23 15:14:33 +00:00
|
|
|
if len(uncles) == 2 {
|
2015-03-23 11:12:49 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := self.commitUncle(uncle.Header()); err != nil {
|
2015-04-15 10:12:20 +00:00
|
|
|
if glog.V(logger.Ridiculousness) {
|
|
|
|
glog.V(logger.Detail).Infof("Bad uncle found and will be removed (%x)\n", hash[:4])
|
|
|
|
glog.V(logger.Detail).Infoln(uncle)
|
|
|
|
}
|
|
|
|
|
2015-03-23 17:27:05 +00:00
|
|
|
badUncles = append(badUncles, hash)
|
2015-03-23 11:12:49 +00:00
|
|
|
} else {
|
2015-04-07 12:57:04 +00:00
|
|
|
glog.V(logger.Debug).Infof("commiting %x as uncle\n", hash[:4])
|
2015-03-23 15:14:33 +00:00
|
|
|
uncles = append(uncles, uncle.Header())
|
2015-03-23 11:12:49 +00:00
|
|
|
}
|
|
|
|
}
|
2015-04-07 12:57:04 +00:00
|
|
|
|
|
|
|
// We only care about logging if we're actually mining
|
|
|
|
if atomic.LoadInt64(&self.mining) == 1 {
|
|
|
|
glog.V(logger.Info).Infof("commit new work on block %v with %d txs & %d uncles\n", self.current.block.Number(), tcount, len(uncles))
|
|
|
|
}
|
|
|
|
|
2015-03-23 17:27:05 +00:00
|
|
|
for _, hash := range badUncles {
|
|
|
|
delete(self.possibleUncles, hash)
|
|
|
|
}
|
2015-03-23 15:35:44 +00:00
|
|
|
|
2015-03-23 15:14:33 +00:00
|
|
|
self.current.block.SetUncles(uncles)
|
2015-03-23 11:12:49 +00:00
|
|
|
|
2015-04-01 19:18:41 +00:00
|
|
|
core.AccumulateRewards(self.current.state, self.current.block)
|
2015-02-04 13:52:59 +00:00
|
|
|
|
2015-04-01 21:58:26 +00:00
|
|
|
self.current.state.Update()
|
2015-04-07 10:32:55 +00:00
|
|
|
|
2015-02-09 15:20:34 +00:00
|
|
|
self.push()
|
2015-02-04 13:52:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
inclusionReward = new(big.Int).Div(core.BlockReward, big.NewInt(32))
|
|
|
|
_uncleReward = new(big.Int).Mul(core.BlockReward, big.NewInt(15))
|
|
|
|
uncleReward = new(big.Int).Div(_uncleReward, big.NewInt(16))
|
|
|
|
)
|
|
|
|
|
|
|
|
func (self *worker) commitUncle(uncle *types.Header) error {
|
2015-03-18 12:00:01 +00:00
|
|
|
if self.current.uncles.Has(uncle.Hash()) {
|
2015-02-04 13:52:59 +00:00
|
|
|
// Error not unique
|
|
|
|
return core.UncleError("Uncle not unique")
|
|
|
|
}
|
2015-03-18 12:00:01 +00:00
|
|
|
self.current.uncles.Add(uncle.Hash())
|
2015-02-04 13:52:59 +00:00
|
|
|
|
2015-03-23 11:12:49 +00:00
|
|
|
if !self.current.family.Has(uncle.ParentHash) {
|
2015-02-04 13:52:59 +00:00
|
|
|
return core.UncleError(fmt.Sprintf("Uncle's parent unknown (%x)", uncle.ParentHash[0:4]))
|
|
|
|
}
|
|
|
|
|
2015-03-23 11:12:49 +00:00
|
|
|
if self.current.family.Has(uncle.Hash()) {
|
|
|
|
return core.UncleError(fmt.Sprintf("Uncle already in family (%x)", uncle.Hash()))
|
2015-02-04 13:52:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (self *worker) commitTransaction(tx *types.Transaction) error {
|
2015-03-11 15:32:37 +00:00
|
|
|
snap := self.current.state.Copy()
|
2015-02-15 15:16:27 +00:00
|
|
|
receipt, _, err := self.proc.ApplyTransaction(self.current.coinbase, self.current.state, self.current.block, tx, self.current.totalUsedGas, true)
|
2015-03-11 15:32:37 +00:00
|
|
|
if err != nil && (core.IsNonceErr(err) || state.IsGasLimitErr(err) || core.IsInvalidTxErr(err)) {
|
|
|
|
self.current.state.Set(snap)
|
2015-02-04 13:52:59 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
self.current.block.AddTransaction(tx)
|
|
|
|
self.current.block.AddReceipt(receipt)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2015-02-28 22:09:49 +00:00
|
|
|
|
|
|
|
func (self *worker) HashRate() int64 {
|
|
|
|
var tot int64
|
|
|
|
for _, agent := range self.agents {
|
2015-03-20 16:42:09 +00:00
|
|
|
tot += agent.GetHashRate()
|
2015-02-28 22:09:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return tot
|
|
|
|
}
|