diff --git a/consensus/ethash/api.go b/consensus/ethash/api.go index 3b7aea3ce2..fb64b85d07 100644 --- a/consensus/ethash/api.go +++ b/consensus/ethash/api.go @@ -1,4 +1,4 @@ -// Copyright 2017 The go-ethereum Authors +// Copyright 2018 The go-ethereum Authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify @@ -16,7 +16,10 @@ package ethash import ( + "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" ) @@ -35,13 +38,12 @@ func (s *API) GetWork() ([3]string, error) { errCh = make(chan error) ) op := &remoteOp{ - typ: getWork, + typ: opGetWork, workCh: workCh, errCh: errCh, } s.ethash.remoteOp <- op - err := <-errCh - if err == nil { + if err := <-errCh; err == nil { return <-workCh, nil } else { return [3]string{}, err @@ -52,15 +54,14 @@ func (s *API) GetWork() ([3]string, error) { // accepted. Note, this is not an indication if the provided work was valid! func (s *API) SubmitWork(nonce types.BlockNonce, solution, digest common.Hash) bool { var errCh = make(chan error) - result := sealResult{ - nonce: nonce, - mixDigest: digest, - hash: solution, - } op := &remoteOp{ - typ: submitWork, - result: result, - errCh: errCh, + typ: opSubmitWork, + result: mineResult{ + nonce: nonce, + mixDigest: digest, + hash: solution, + }, + errCh: errCh, } s.ethash.remoteOp <- op if err := <-errCh; err == nil { @@ -69,3 +70,21 @@ func (s *API) SubmitWork(nonce types.BlockNonce, solution, digest common.Hash) b return false } } + +// SubmitHashrate can be used for remote miners to submit their hash rate. This enables the node to report the combined +// hash rate of all miners which submit work through this node. It accepts the miner hash rate and an identifier which +// must be unique between nodes. +func (s *API) SubmitHashRate(r hexutil.Uint64, id common.Hash) bool { + submit := func(rate map[common.Hash]hashrate) { + rate[id] = hashrate{rate: uint64(r), ping: time.Now()} + } + errCh := make(chan error) + op := &remoteOp{ + typ: opHashrate, + rateOp: submit, + errCh: errCh, + } + s.ethash.remoteOp <- op + <-errCh + return true +} diff --git a/consensus/ethash/ethash.go b/consensus/ethash/ethash.go index 0c2bccb9a2..2fccc57734 100644 --- a/consensus/ethash/ethash.go +++ b/consensus/ethash/ethash.go @@ -395,23 +395,34 @@ type Config struct { type remoteOpType uint const ( - getWork remoteOpType = iota - submitWork + opGetWork remoteOpType = iota + opSubmitWork + opHashrate ) -// sealResult wraps the pow solution parameters for the specified block. -type sealResult struct { +// mineResult wraps the pow solution parameters for the specified block. +type mineResult struct { nonce types.BlockNonce mixDigest common.Hash hash common.Hash } +type hashrate struct { + id common.Hash + ping time.Time + rate uint64 +} + +type hashrateOp func(map[common.Hash]hashrate) + // remoteOp wraps a mining operation sent by remote miner. type remoteOp struct { typ remoteOpType - result sealResult + result mineResult errCh chan error workCh chan [3]string + + rateOp hashrateOp } // Ethash is a consensus engine based on proof-of-work implementing the ethash @@ -434,7 +445,6 @@ type Ethash struct { workCh chan *types.Block // Notification channel to push new work to remote sealer resultCh chan *types.Block // Channel used by mining threads to return result remoteOp chan *remoteOp // Channel used to receive all mining operations from api - // The fields below are hooks for testing shared *Ethash // Shared PoW verifier to avoid cache regeneration fakeFail uint64 // Block number which fails PoW check even in fake mode @@ -474,7 +484,20 @@ func New(config Config) *Ethash { // NewTester creates a small sized ethash PoW scheme useful only for testing // purposes. func NewTester() *Ethash { - return New(Config{CachesInMem: 1, PowMode: ModeTest}) + ethash := &Ethash{ + config: Config{PowMode: ModeTest}, + caches: newlru("cache", 1, newCache), + datasets: newlru("dataset", 1, newDataset), + update: make(chan struct{}), + hashrate: metrics.NewMeter(), + exitCh: make(chan struct{}), + workCh: make(chan *types.Block), + resultCh: make(chan *types.Block), + remoteOp: make(chan *remoteOp), + } + go ethash.remote(ethash.resultCh) + runtime.SetFinalizer(ethash, (*Ethash).close) + return ethash } // NewFaker creates a ethash consensus engine with a fake PoW scheme that accepts @@ -606,14 +629,36 @@ func (ethash *Ethash) SetThreads(threads int) { // Hashrate implements PoW, returning the measured rate of the search invocations // per second over the last minute. +// Note the returned hashrate includes local hashrate, but also includes the total +// hashrate of all remote miner. func (ethash *Ethash) Hashrate() float64 { - return ethash.hashrate.Rate1() + var ( + total uint64 + errCh = make(chan error) + ) + // getHashrate gathers all remote miner's hashrate. + getHashrate := func(hashrate map[common.Hash]hashrate) { + for _, rate := range hashrate { + // this could overflow + total += rate.rate + } + return + } + op := &remoteOp{ + typ: opHashrate, + rateOp: getHashrate, + errCh: errCh, + } + ethash.remoteOp <- op + <-errCh + + return ethash.hashrate.Rate1() + float64(total) } // APIs implements consensus.Engine, returning the user facing RPC APIs. func (ethash *Ethash) APIs(chain consensus.ChainReader) []rpc.API { return []rpc.API{{ - Namespace: "ethash", + Namespace: "eth", Version: "1.0", Service: &API{ethash}, Public: true, diff --git a/consensus/ethash/ethash_test.go b/consensus/ethash/ethash_test.go index 31116da437..b5d8ae9022 100644 --- a/consensus/ethash/ethash_test.go +++ b/consensus/ethash/ethash_test.go @@ -24,6 +24,8 @@ import ( "sync" "testing" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" ) @@ -69,7 +71,7 @@ func verifyTest(wg *sync.WaitGroup, e *Ethash, workerIndex, epochs int) { const wiggle = 4 * epochLength r := rand.New(rand.NewSource(int64(workerIndex))) for epoch := 0; epoch < epochs; epoch++ { - block := int64(epoch)*epochLength - wiggle/2 + r.Int63n(wiggle) + block := int64(epoch) * epochLength - wiggle / 2 + r.Int63n(wiggle) if block < 0 { block = 0 } @@ -77,3 +79,47 @@ func verifyTest(wg *sync.WaitGroup, e *Ethash, workerIndex, epochs int) { e.VerifySeal(nil, head) } } + +func TestRemoteSealer(t *testing.T) { + ethash := NewTester() + api := &API{ethash} + if _, err := api.GetWork(); err != errNoMineWork { + t.Error("expected to return an error indicate there is no mining work") + } + + head := &types.Header{Number: big.NewInt(1), Difficulty: big.NewInt(100)} + block := types.NewBlockWithHeader(head) + // Push new work + ethash.Seal(nil, block, nil) + + var ( + work [3]string + err error + ) + if work, err = api.GetWork(); err != nil || work[0] != block.HashNoNonce().Hex() { + t.Error("expected to return a mining work has same hash") + } + + if res := api.SubmitWork(types.BlockNonce{}, block.HashNoNonce(), common.Hash{}); res { + t.Error("expected to return false when submit a fake solution") + } +} + +func TestHashRate(t *testing.T) { + var ( + ethash = NewTester() + api = &API{ethash} + hashrate = []hexutil.Uint64{100, 200, 300} + expect uint64 + ids = []common.Hash{common.HexToHash("a"), common.HexToHash("b"), common.HexToHash("c")} + ) + for i := 0; i < len(hashrate); i += 1 { + if res := api.SubmitHashRate(hashrate[i], ids[i]); !res { + t.Error("remote miner submit hashrate failed") + } + expect += uint64(hashrate[i]) + } + if tot := ethash.Hashrate(); tot != float64(expect) { + t.Error("expect total hashrate should be same") + } +} diff --git a/consensus/ethash/sealer.go b/consensus/ethash/sealer.go index 5d3cd8e94c..4d34f78cf7 100644 --- a/consensus/ethash/sealer.go +++ b/consensus/ethash/sealer.go @@ -18,13 +18,14 @@ package ethash import ( crand "crypto/rand" + "errors" "math" "math/big" "math/rand" "runtime" "sync" + "time" - "errors" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/types" @@ -32,9 +33,9 @@ import ( ) var ( - errNoSealWork = errors.New("No work available yet, don't panic.") - errInvalidSealResult = errors.New("Invalid proof-of-work solution.") - errNonDefinedRemoteOp = errors.New("Non-defined remote mining operation.") + errNoMineWork = errors.New("No mining work available yet, don't panic.") + errInvalidSealResult = errors.New("Invalid or stale proof-of-work solution.") + errUndefinedRemoteOp = errors.New("Undefined remote mining operation.") ) // Seal implements consensus.Engine, attempting to find a nonce that satisfies @@ -72,15 +73,6 @@ func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop threads = 0 // Allows disabling local mining without extra logic around local/remote } - // Clear up result channel before new round mining start - for empty := false; !empty; { - select { - case <-ethash.resultCh: - default: - empty = true - } - } - // Push new work to remote sealer ethash.workCh <- block @@ -175,17 +167,18 @@ search: func (ethash *Ethash) remote(resultCh chan *types.Block) { var ( works = make(map[common.Hash]*types.Block) + hashrate = make(map[common.Hash]hashrate) currentWork *types.Block ) - // makeWork returns a work package for external sealer. The work package consists of 3 strings + // getWork returns a work package for external miner. The work package consists of 3 strings // result[0], 32 bytes hex encoded current block header pow-hash // result[1], 32 bytes hex encoded seed hash used for DAG // result[2], 32 bytes hex encoded boundary condition ("target"), 2^256/difficulty - makeWork := func() ([3]string, error) { + getWork := func() ([3]string, error) { var res [3]string if currentWork == nil { - return res, errNoSealWork + return res, errNoMineWork } res[0] = currentWork.HashNoNonce().Hex() res[1] = common.BytesToHash(SeedHash(currentWork.NumberU64())).Hex() @@ -199,10 +192,10 @@ func (ethash *Ethash) remote(resultCh chan *types.Block) { return res, nil } - // verifyWork verifies the submitted pow solution, returning + // submitWork verifies the submitted pow solution, returning // whether the solution was accepted or not (not can be both a bad pow as well as // any other error, like no work pending). - verifyWork := func(result sealResult) bool { + submitWork := func(result mineResult) bool { // Make sure the work submitted is present block := works[result.hash] if block == nil { @@ -220,42 +213,65 @@ func (ethash *Ethash) remote(resultCh chan *types.Block) { } // Solutions seems to be valid, return to the miner and notify acceptance - resultCh <- block.WithSeal(header) - delete(works, result.hash) - return true + select { + case resultCh <- block.WithSeal(header): + delete(works, result.hash) + return true + default: + log.Info("Work submitted is stale", "hash", result.hash) + return false + } } + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + running: for { select { case block := <-ethash.workCh: - if block.ParentHash() != currentWork.ParentHash() { + if currentWork != nil && block.ParentHash() != currentWork.ParentHash() { // Start new round mining, throw out all previous work. works = make(map[common.Hash]*types.Block) } // Update current work with new received block + // Note same work can be past twice, happens when changing CPU threads. currentWork = block case op := <-ethash.remoteOp: switch op.typ { - case getWork: - // Push current mining work to return channel - work, err := makeWork() + case opGetWork: + // Return current mining work to remote miner + work, err := getWork() if err != nil { op.errCh <- err } else { - op.workCh <- work close(op.errCh) + op.workCh <- work } - case submitWork: + case opSubmitWork: // Verify submitted PoW solution based on maintained mining blocks - if verifyWork(op.result) { + if submitWork(op.result) { close(op.errCh) } else { op.errCh <- errInvalidSealResult } + case opHashrate: + // This case is used by remote miner hashrate operation + if op.rateOp != nil { + op.rateOp(hashrate) + close(op.errCh) + } default: - op.errCh <- errNonDefinedRemoteOp + op.errCh <- errUndefinedRemoteOp + } + + case <-ticker.C: + // Clear stale submited hashrate + for id, rate := range hashrate { + if time.Since(rate.ping) > 10*time.Second { + delete(hashrate, id) + } } case <-ethash.exitCh: diff --git a/eth/api.go b/eth/api.go index 0b6da456f6..ef66e99cbf 100644 --- a/eth/api.go +++ b/eth/api.go @@ -34,7 +34,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" @@ -70,16 +69,12 @@ func (api *PublicEthereumAPI) Hashrate() hexutil.Uint64 { // PublicMinerAPI provides an API to control the miner. // It offers only methods that operate on data that pose no security risk when it is publicly accessible. type PublicMinerAPI struct { - e *Ethereum - agent *miner.RemoteAgent + e *Ethereum } // NewPublicMinerAPI create a new PublicMinerAPI instance. func NewPublicMinerAPI(e *Ethereum) *PublicMinerAPI { - agent := miner.NewRemoteAgent(e.BlockChain(), e.Engine()) - e.Miner().Register(agent) - - return &PublicMinerAPI{e, agent} + return &PublicMinerAPI{e} } // Mining returns an indication if this node is currently mining. @@ -87,37 +82,6 @@ func (api *PublicMinerAPI) Mining() bool { return api.e.IsMining() } -// SubmitWork can be used by external miner to submit their POW solution. It returns an indication if the work was -// accepted. Note, this is not an indication if the provided work was valid! -func (api *PublicMinerAPI) SubmitWork(nonce types.BlockNonce, solution, digest common.Hash) bool { - return api.agent.SubmitWork(nonce, digest, solution) -} - -// GetWork returns a work package for external miner. The work package consists of 3 strings -// result[0], 32 bytes hex encoded current block header pow-hash -// result[1], 32 bytes hex encoded seed hash used for DAG -// result[2], 32 bytes hex encoded boundary condition ("target"), 2^256/difficulty -func (api *PublicMinerAPI) GetWork() ([3]string, error) { - if !api.e.IsMining() { - if err := api.e.StartMining(false); err != nil { - return [3]string{}, err - } - } - work, err := api.agent.GetWork() - if err != nil { - return work, fmt.Errorf("mining not ready: %v", err) - } - return work, nil -} - -// SubmitHashrate can be used for remote miners to submit their hash rate. This enables the node to report the combined -// hash rate of all miners which submit work through this node. It accepts the miner hash rate and an identifier which -// must be unique between nodes. -func (api *PublicMinerAPI) SubmitHashrate(hashrate hexutil.Uint64, id common.Hash) bool { - api.agent.SubmitHashrate(id, uint64(hashrate)) - return true -} - // PrivateMinerAPI provides private RPC methods to control the miner. // These methods can be abused by external users and must be considered insecure for use by untrusted users. type PrivateMinerAPI struct { diff --git a/miner/agent.go b/miner/agent.go index e3cebbd2e2..bb062c3ecb 100644 --- a/miner/agent.go +++ b/miner/agent.go @@ -18,7 +18,6 @@ package miner import ( "sync" - "sync/atomic" "github.com/ethereum/go-ethereum/consensus" @@ -110,10 +109,3 @@ func (self *CpuAgent) mine(work *Work, stop <-chan struct{}) { self.returnCh <- nil } } - -func (self *CpuAgent) GetHashRate() int64 { - if pow, ok := self.engine.(consensus.PoW); ok { - return int64(pow.Hashrate()) - } - return 0 -} diff --git a/miner/miner.go b/miner/miner.go index d9256e9787..3a744e846a 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -143,14 +143,6 @@ func (self *Miner) HashRate() (tot int64) { if pow, ok := self.engine.(consensus.PoW); ok { tot += int64(pow.Hashrate()) } - // do we care this might race? is it worth we're rewriting some - // aspects of the worker/locking up agents so we can get an accurate - // hashrate? - for agent := range self.worker.agents { - if _, ok := agent.(*CpuAgent); !ok { - tot += agent.GetHashRate() - } - } return } diff --git a/miner/remote_agent.go b/miner/remote_agent.go deleted file mode 100644 index 287e7530c3..0000000000 --- a/miner/remote_agent.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package miner - -import ( - "errors" - "math/big" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" -) - -type hashrate struct { - ping time.Time - rate uint64 -} - -type RemoteAgent struct { - mu sync.Mutex - - quitCh chan struct{} - workCh chan *Work - returnCh chan<- *Result - - chain consensus.ChainReader - engine consensus.Engine - currentWork *Work - work map[common.Hash]*Work - - hashrateMu sync.RWMutex - hashrate map[common.Hash]hashrate - - running int32 // running indicates whether the agent is active. Call atomically -} - -func NewRemoteAgent(chain consensus.ChainReader, engine consensus.Engine) *RemoteAgent { - return &RemoteAgent{ - chain: chain, - engine: engine, - work: make(map[common.Hash]*Work), - hashrate: make(map[common.Hash]hashrate), - } -} - -func (a *RemoteAgent) SubmitHashrate(id common.Hash, rate uint64) { - a.hashrateMu.Lock() - defer a.hashrateMu.Unlock() - - a.hashrate[id] = hashrate{time.Now(), rate} -} - -func (a *RemoteAgent) Work() chan<- *Work { - return a.workCh -} - -func (a *RemoteAgent) SetReturnCh(returnCh chan<- *Result) { - a.returnCh = returnCh -} - -func (a *RemoteAgent) Start() { - if !atomic.CompareAndSwapInt32(&a.running, 0, 1) { - return - } - a.quitCh = make(chan struct{}) - a.workCh = make(chan *Work, 1) - go a.loop(a.workCh, a.quitCh) -} - -func (a *RemoteAgent) Stop() { - if !atomic.CompareAndSwapInt32(&a.running, 1, 0) { - return - } - close(a.quitCh) - close(a.workCh) -} - -// GetHashRate returns the accumulated hashrate of all identifier combined -func (a *RemoteAgent) GetHashRate() (tot int64) { - a.hashrateMu.RLock() - defer a.hashrateMu.RUnlock() - - // this could overflow - for _, hashrate := range a.hashrate { - tot += int64(hashrate.rate) - } - return -} - -func (a *RemoteAgent) GetWork() ([3]string, error) { - a.mu.Lock() - defer a.mu.Unlock() - - var res [3]string - - if a.currentWork != nil { - block := a.currentWork.Block - - res[0] = block.HashNoNonce().Hex() - seedHash := ethash.SeedHash(block.NumberU64()) - res[1] = common.BytesToHash(seedHash).Hex() - // Calculate the "target" to be returned to the external miner - n := big.NewInt(1) - n.Lsh(n, 255) - n.Div(n, block.Difficulty()) - n.Lsh(n, 1) - res[2] = common.BytesToHash(n.Bytes()).Hex() - - a.work[block.HashNoNonce()] = a.currentWork - return res, nil - } - return res, errors.New("No work available yet, don't panic.") -} - -// SubmitWork tries to inject a pow solution into the remote agent, returning -// whether the solution was accepted or not (not can be both a bad pow as well as -// any other error, like no work pending). -func (a *RemoteAgent) SubmitWork(nonce types.BlockNonce, mixDigest, hash common.Hash) bool { - a.mu.Lock() - defer a.mu.Unlock() - - // Make sure the work submitted is present - work := a.work[hash] - if work == nil { - log.Info("Work submitted but none pending", "hash", hash) - return false - } - // Make sure the Engine solutions is indeed valid - result := work.Block.Header() - result.Nonce = nonce - result.MixDigest = mixDigest - - if err := a.engine.VerifySeal(a.chain, result); err != nil { - log.Warn("Invalid proof-of-work submitted", "hash", hash, "err", err) - return false - } - block := work.Block.WithSeal(result) - - // Solutions seems to be valid, return to the miner and notify acceptance - a.returnCh <- &Result{work, block} - delete(a.work, hash) - - return true -} - -// loop monitors mining events on the work and quit channels, updating the internal -// state of the remote miner until a termination is requested. -// -// Note, the reason the work and quit channels are passed as parameters is because -// RemoteAgent.Start() constantly recreates these channels, so the loop code cannot -// assume data stability in these member fields. -func (a *RemoteAgent) loop(workCh chan *Work, quitCh chan struct{}) { - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-quitCh: - return - case work := <-workCh: - a.mu.Lock() - a.currentWork = work - a.mu.Unlock() - case <-ticker.C: - // cleanup - a.mu.Lock() - for hash, work := range a.work { - if time.Since(work.createdAt) > 7*(12*time.Second) { - delete(a.work, hash) - } - } - a.mu.Unlock() - - a.hashrateMu.Lock() - for id, hashrate := range a.hashrate { - if time.Since(hashrate.ping) > 10*time.Second { - delete(a.hashrate, id) - } - } - a.hashrateMu.Unlock() - } - } -} diff --git a/miner/worker.go b/miner/worker.go index 34329f8499..624be1d0e6 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -57,7 +57,6 @@ type Agent interface { SetReturnCh(chan<- *Result) Stop() Start() - GetHashRate() int64 } // Work is the workers current environment and holds