diff --git a/cmd/geth/consolecmd_test.go b/cmd/geth/consolecmd_test.go index 8d8b10f8fa..34ba877020 100644 --- a/cmd/geth/consolecmd_test.go +++ b/cmd/geth/consolecmd_test.go @@ -31,7 +31,7 @@ import ( ) const ( - ipcAPIs = "admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 shh:1.0 txpool:1.0 web3:1.0" + ipcAPIs = "admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 shh:1.0 txpool:1.0 web3:1.0" httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0" ) diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 8968f500f1..5963900c97 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -672,6 +672,11 @@ func CalcDifficulty(snap *Snapshot, signer common.Address) *big.Int { return new(big.Int).Set(diffNoTurn) } +// Close implements consensus.Engine, returning internal error and close the clique. +func (c *Clique) Close() error { + return nil +} + // APIs implements consensus.Engine, returning the user facing RPC API to allow // controlling the signer voting. func (c *Clique) APIs(chain consensus.ChainReader) []rpc.API { diff --git a/consensus/consensus.go b/consensus/consensus.go index 5774af1a78..ae0fefb490 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -96,6 +96,9 @@ type Engine interface { // APIs returns the RPC APIs this consensus engine provides. APIs(chain ChainReader) []rpc.API + + // Close closes the consensus engine. + Close() error } // PoW is a consensus engine based on proof-of-work. diff --git a/consensus/ethash/algorithm_test.go b/consensus/ethash/algorithm_test.go index f0c6465fdb..e7625f7c00 100644 --- a/consensus/ethash/algorithm_test.go +++ b/consensus/ethash/algorithm_test.go @@ -730,6 +730,7 @@ func TestConcurrentDiskCacheGeneration(t *testing.T) { go func(idx int) { defer pend.Done() ethash := New(Config{cachedir, 0, 1, "", 0, 0, ModeNormal}) + defer ethash.Close() if err := ethash.VerifySeal(nil, block.Header()); err != nil { t.Errorf("proc %d: block verification failed: %v", idx, err) } diff --git a/consensus/ethash/api.go b/consensus/ethash/api.go index fb64b85d07..2e5c2c8f2d 100644 --- a/consensus/ethash/api.go +++ b/consensus/ethash/api.go @@ -13,78 +13,87 @@ // // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . + package ethash import ( - "time" + "errors" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" ) -// API exposes ethash related methods for the RPC interface +var errEthashStopped = errors.New("ethash stopped") + +// API exposes ethash related methods for the RPC interface. type API struct { ethash *Ethash } -// 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 (s *API) GetWork() ([3]string, error) { +// 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 *API) GetWork() ([3]string, error) { var ( - workCh = make(chan [3]string) - errCh = make(chan error) + workCh = make(chan [3]string, 1) + errCh = make(chan error, 1) + err error ) - op := &remoteOp{ - typ: opGetWork, - workCh: workCh, - errCh: errCh, + + select { + case api.ethash.fetchWorkCh <- &sealWork{errCh: errCh, resCh: workCh}: + case <-api.ethash.exitCh: + return [3]string{}, errEthashStopped } - s.ethash.remoteOp <- op - if err := <-errCh; err == nil { + + if err = <-errCh; err == nil { return <-workCh, nil - } else { - return [3]string{}, err } + return [3]string{}, err } -// 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 (s *API) SubmitWork(nonce types.BlockNonce, solution, digest common.Hash) bool { - var errCh = make(chan error) - op := &remoteOp{ - typ: opSubmitWork, - result: mineResult{ - nonce: nonce, - mixDigest: digest, - hash: solution, - }, - errCh: errCh, - } - s.ethash.remoteOp <- op - if err := <-errCh; err == nil { - return true - } else { +// SubmitWork can be used by external miner to submit their POW solution. +// It returns an indication if the work was accepted. +// Note either an invalid solution, a stale work a non-existent work will return false. +func (api *API) SubmitWork(nonce types.BlockNonce, hash, digest common.Hash) bool { + var errCh = make(chan error, 1) + + select { + case api.ethash.submitWorkCh <- &mineResult{ + nonce: nonce, + mixDigest: digest, + hash: hash, + errCh: errCh, + }: + case <-api.ethash.exitCh: return false } + + err := <-errCh + return err == 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 (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()} +// 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 *API) SubmitHashRate(rate hexutil.Uint64, id common.Hash) bool { + var doneCh = make(chan struct{}, 1) + + select { + case api.ethash.submitRateCh <- &hashrate{done: doneCh, rate: uint64(rate), id: id}: + case <-api.ethash.exitCh: + return false } - errCh := make(chan error) - op := &remoteOp{ - typ: opHashrate, - rateOp: submit, - errCh: errCh, - } - s.ethash.remoteOp <- op - <-errCh + + // Block until hash rate submitted successfully. + <-doneCh + return true } diff --git a/consensus/ethash/ethash.go b/consensus/ethash/ethash.go index 2fccc57734..424f779917 100644 --- a/consensus/ethash/ethash.go +++ b/consensus/ethash/ethash.go @@ -391,38 +391,28 @@ type Config struct { PowMode Mode } -// remoteOpType defines all the types of operations related to remote sealer. -type remoteOpType uint - -const ( - opGetWork remoteOpType = iota - opSubmitWork - opHashrate -) - // mineResult wraps the pow solution parameters for the specified block. type mineResult struct { nonce types.BlockNonce mixDigest common.Hash hash common.Hash + + errCh chan error } +// hashrate wraps the hash rate submitted by the remote sealer. type hashrate struct { id common.Hash ping time.Time rate uint64 + + done chan struct{} } -type hashrateOp func(map[common.Hash]hashrate) - -// remoteOp wraps a mining operation sent by remote miner. -type remoteOp struct { - typ remoteOpType - result mineResult - errCh chan error - workCh chan [3]string - - rateOp hashrateOp +// sealWork wraps a seal work package for remote sealer. +type sealWork struct { + errCh chan error + resCh chan [3]string } // Ethash is a consensus engine based on proof-of-work implementing the ethash @@ -433,8 +423,6 @@ type Ethash struct { caches *lru // In memory caches to avoid regenerating too often datasets *lru // In memory datasets to avoid regenerating too often - exitCh chan struct{} // Notification channel to exiting backend threads - // Mining related fields rand *rand.Rand // Properly seeded random source for nonces threads int // Number of threads to mine on if mining @@ -442,15 +430,21 @@ type Ethash struct { hashrate metrics.Meter // Meter tracking the average hashrate // Remote sealer related fields - 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 + 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 + fetchWorkCh chan *sealWork // Channel used for remote sealer to fetch mining work + submitWorkCh chan *mineResult // Channel used for remote sealer to submit their mining result + fetchRateCh chan chan uint64 // Channel used to gather submitted hash rate for local or remote sealer. + submitRateCh chan *hashrate // Channel used for remote sealer to submit their mining hashrate + // 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 fakeDelay time.Duration // Time delay to sleep for before returning from verify - lock sync.Mutex // Ensures thread safety for the in-memory caches and mining fields + lock sync.Mutex // Ensures thread safety for the in-memory caches and mining fields + closeOnce sync.Once // Ensures exit channel will not be closed twice. + exitCh chan chan error // Notification channel to exiting backend threads } // New creates a full sized ethash PoW scheme. @@ -466,18 +460,20 @@ func New(config Config) *Ethash { log.Info("Disk storage enabled for ethash DAGs", "dir", config.DatasetDir, "count", config.DatasetsOnDisk) } ethash := &Ethash{ - config: config, - caches: newlru("cache", config.CachesInMem, newCache), - datasets: newlru("dataset", config.DatasetsInMem, 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), + config: config, + caches: newlru("cache", config.CachesInMem, newCache), + datasets: newlru("dataset", config.DatasetsInMem, newDataset), + update: make(chan struct{}), + hashrate: metrics.NewMeter(), + workCh: make(chan *types.Block), + resultCh: make(chan *types.Block), + fetchWorkCh: make(chan *sealWork), + submitWorkCh: make(chan *mineResult), + fetchRateCh: make(chan chan uint64), + submitRateCh: make(chan *hashrate), + exitCh: make(chan chan error), } - go ethash.remote(ethash.resultCh) - runtime.SetFinalizer(ethash, (*Ethash).close) + go ethash.remote() return ethash } @@ -485,18 +481,20 @@ func New(config Config) *Ethash { // purposes. func NewTester() *Ethash { 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), + config: Config{PowMode: ModeTest}, + caches: newlru("cache", 1, newCache), + datasets: newlru("dataset", 1, newDataset), + update: make(chan struct{}), + hashrate: metrics.NewMeter(), + workCh: make(chan *types.Block), + resultCh: make(chan *types.Block), + fetchWorkCh: make(chan *sealWork), + submitWorkCh: make(chan *mineResult), + fetchRateCh: make(chan chan uint64), + submitRateCh: make(chan *hashrate), + exitCh: make(chan chan error), } - go ethash.remote(ethash.resultCh) - runtime.SetFinalizer(ethash, (*Ethash).close) + go ethash.remote() return ethash } @@ -508,6 +506,7 @@ func NewFaker() *Ethash { config: Config{ PowMode: ModeFake, }, + exitCh: make(chan chan error), } } @@ -520,6 +519,7 @@ func NewFakeFailer(fail uint64) *Ethash { PowMode: ModeFake, }, fakeFail: fail, + exitCh: make(chan chan error), } } @@ -532,6 +532,7 @@ func NewFakeDelayer(delay time.Duration) *Ethash { PowMode: ModeFake, }, fakeDelay: delay, + exitCh: make(chan chan error), } } @@ -542,19 +543,32 @@ func NewFullFaker() *Ethash { config: Config{ PowMode: ModeFullFake, }, + exitCh: make(chan chan error), } } // NewShared creates a full sized ethash PoW shared between all requesters running // in the same process. func NewShared() *Ethash { - return &Ethash{shared: sharedEthash} + return &Ethash{ + shared: sharedEthash, + exitCh: make(chan chan error), + } } -// close closes the exit channel to notify all backend threads exiting. -func (ethash *Ethash) close() { - runtime.SetFinalizer(ethash, nil) - close(ethash.exitCh) +// Close closes the exit channel to notify all backend threads exiting. +func (ethash *Ethash) Close() error { + var err error + ethash.closeOnce.Do(func() { + var errCh = make(chan error) + select { + case ethash.exitCh <- errCh: + err = <-errCh + close(ethash.exitCh) + default: + } + }) + return err } // cache tries to retrieve a verification cache for the specified block number @@ -632,37 +646,38 @@ func (ethash *Ethash) SetThreads(threads int) { // Note the returned hashrate includes local hashrate, but also includes the total // hashrate of all remote miner. func (ethash *Ethash) Hashrate() float64 { - 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 + var resCh = make(chan uint64, 1) + select { + case ethash.fetchRateCh <- resCh: + case <-ethash.exitCh: + // Return local hashrate only if ethash is stopped. + return ethash.hashrate.Rate1() + } + + // Gather total submitted hash rate of remote sealers. + total := <-resCh 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: "eth", - Version: "1.0", - Service: &API{ethash}, - Public: true, - }} + // In order to ensure backward compatibility, we exposes ethash RPC APIs + // to both eth and ethash namespaces. + return []rpc.API{ + { + Namespace: "eth", + Version: "1.0", + Service: &API{ethash}, + Public: true, + }, + { + Namespace: "ethash", + Version: "1.0", + Service: &API{ethash}, + Public: true, + }, + } } // SeedHash is the seed to use for generating a verification cache and the mining diff --git a/consensus/ethash/ethash_test.go b/consensus/ethash/ethash_test.go index b5d8ae9022..ccdd30fb0f 100644 --- a/consensus/ethash/ethash_test.go +++ b/consensus/ethash/ethash_test.go @@ -23,6 +23,7 @@ import ( "os" "sync" "testing" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -34,6 +35,7 @@ func TestTestMode(t *testing.T) { head := &types.Header{Number: big.NewInt(1), Difficulty: big.NewInt(100)} ethash := NewTester() + defer ethash.Close() block, err := ethash.Seal(nil, types.NewBlockWithHeader(head), nil) if err != nil { t.Fatalf("failed to seal block: %v", err) @@ -54,6 +56,7 @@ func TestCacheFileEvict(t *testing.T) { } defer os.RemoveAll(tmpdir) e := New(Config{CachesInMem: 3, CachesOnDisk: 10, CacheDir: tmpdir, PowMode: ModeTest}) + defer e.Close() workers := 8 epochs := 100 @@ -71,7 +74,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 } @@ -82,14 +85,16 @@ func verifyTest(wg *sync.WaitGroup, e *Ethash, workerIndex, epochs int) { func TestRemoteSealer(t *testing.T) { ethash := NewTester() + defer ethash.Close() api := &API{ethash} - if _, err := api.GetWork(); err != errNoMineWork { - t.Error("expected to return an error indicate there is no mining work") + if _, err := api.GetWork(); err != errNoMiningWork { + t.Error("expect 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 + + // Push new work. ethash.Seal(nil, block, nil) var ( @@ -97,11 +102,29 @@ func TestRemoteSealer(t *testing.T) { 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") + t.Error("expect 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") + t.Error("expect to return false when submit a fake solution") + } + + // Push new block with same block number to replace the original one. + head = &types.Header{Number: big.NewInt(1), Difficulty: big.NewInt(1000)} + block = types.NewBlockWithHeader(head) + ethash.Seal(nil, block, nil) + + if work, err = api.GetWork(); err != nil || work[0] != block.HashNoNonce().Hex() { + t.Error("expect to return the latest pushed work") + } + + // Push block with higher block number. + newHead := &types.Header{Number: big.NewInt(2), Difficulty: big.NewInt(100)} + newBlock := types.NewBlockWithHeader(newHead) + ethash.Seal(nil, newBlock, nil) + + if res := api.SubmitWork(types.BlockNonce{}, block.HashNoNonce(), common.Hash{}); res { + t.Error("expect to return false when submit a stale solution") } } @@ -113,6 +136,13 @@ func TestHashRate(t *testing.T) { expect uint64 ids = []common.Hash{common.HexToHash("a"), common.HexToHash("b"), common.HexToHash("c")} ) + + defer ethash.Close() + + if tot := ethash.Hashrate(); tot != 0 { + t.Error("expect the result should be zero") + } + for i := 0; i < len(hashrate); i += 1 { if res := api.SubmitHashRate(hashrate[i], ids[i]); !res { t.Error("remote miner submit hashrate failed") @@ -123,3 +153,19 @@ func TestHashRate(t *testing.T) { t.Error("expect total hashrate should be same") } } + +func TestClosedRemoteSealer(t *testing.T) { + ethash := NewTester() + // Make sure exit channel has been listened + time.Sleep(1 * time.Second) + ethash.Close() + + api := &API{ethash} + if _, err := api.GetWork(); err != errEthashStopped { + t.Error("expect to return an error to indicate ethash is stopped") + } + + if res := api.SubmitHashRate(hexutil.Uint64(100), common.HexToHash("a")); res { + t.Error("expect to return false when submit hashrate to a stopped ethash") + } +} diff --git a/consensus/ethash/sealer.go b/consensus/ethash/sealer.go index 4d34f78cf7..a843fab672 100644 --- a/consensus/ethash/sealer.go +++ b/consensus/ethash/sealer.go @@ -33,9 +33,8 @@ import ( ) var ( - 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.") + errNoMiningWork = errors.New("no mining work available yet, don't panic") + errInvalidSealResult = errors.New("invalid or stale proof-of-work solution") ) // Seal implements consensus.Engine, attempting to find a nonce that satisfies @@ -51,7 +50,6 @@ func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop if ethash.shared != nil { return ethash.shared.Seal(chain, block, stop) } - // Create a runner and the multiple search threads it directs abort := make(chan struct{}) @@ -72,10 +70,10 @@ func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop if threads < 0 { threads = 0 // Allows disabling local mining without extra logic around local/remote } - // Push new work to remote sealer - ethash.workCh <- block - + if ethash.workCh != nil { + ethash.workCh <- block + } var pend sync.WaitGroup for i := 0; i < threads; i++ { pend.Add(1) @@ -164,61 +162,72 @@ search: } // remote starts a standalone goroutine to handle remote mining related stuff. -func (ethash *Ethash) remote(resultCh chan *types.Block) { +func (ethash *Ethash) remote() { var ( works = make(map[common.Hash]*types.Block) - hashrate = make(map[common.Hash]hashrate) + rates = make(map[common.Hash]hashrate) currentWork *types.Block ) - // 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 + // 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 getWork := func() ([3]string, error) { var res [3]string if currentWork == nil { - return res, errNoMineWork + return res, errNoMiningWork } res[0] = currentWork.HashNoNonce().Hex() res[1] = common.BytesToHash(SeedHash(currentWork.NumberU64())).Hex() - // Calculate the "target" to be returned to the external sealer + + // Calculate the "target" to be returned to the external sealer. n := big.NewInt(1) n.Lsh(n, 255) n.Div(n, currentWork.Difficulty()) n.Lsh(n, 1) res[2] = common.BytesToHash(n.Bytes()).Hex() + + // Trace the seal work fetched by remote sealer. works[currentWork.HashNoNonce()] = currentWork return res, nil } // 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). - submitWork := func(result mineResult) bool { + // any other error, like no pending work or stale mining result). + submitWork := func(nonce types.BlockNonce, mixDigest common.Hash, hash common.Hash) bool { // Make sure the work submitted is present - block := works[result.hash] + block := works[hash] if block == nil { - log.Info("Work submitted but none pending", "hash", result.hash) + log.Info("Work submitted but none pending", "hash", hash) return false } - // Make sure the Engine solutions is indeed valid + + // Verify the correctness of submitted result. header := block.Header() - header.Nonce = result.nonce - header.MixDigest = result.mixDigest - + header.Nonce = nonce + header.MixDigest = mixDigest if err := ethash.VerifySeal(nil, header); err != nil { - log.Warn("Invalid proof-of-work submitted", "hash", result.hash, "err", err) + log.Warn("Invalid proof-of-work submitted", "hash", hash, "err", err) return false } - // Solutions seems to be valid, return to the miner and notify acceptance + // Make sure the result channel is created. + if ethash.resultCh == nil { + log.Warn("Ethash result channel is empty, submitted mining result is rejected") + return false + } + + // Solutions seems to be valid, return to the miner and notify acceptance. select { - case resultCh <- block.WithSeal(header): - delete(works, result.hash) + case ethash.resultCh <- block.WithSeal(header): + delete(works, hash) return true default: - log.Info("Work submitted is stale", "hash", result.hash) + log.Info("Work submitted is stale", "hash", hash) return false } } @@ -234,48 +243,53 @@ running: // Start new round mining, throw out all previous work. works = make(map[common.Hash]*types.Block) } - // Update current work with new received 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 opGetWork: - // Return current mining work to remote miner - work, err := getWork() - if err != nil { - op.errCh <- err - } else { - close(op.errCh) - op.workCh <- work - } - case opSubmitWork: - // Verify submitted PoW solution based on maintained mining blocks - 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 <- errUndefinedRemoteOp + case work := <-ethash.fetchWorkCh: + // Return current mining work to remote miner. + miningWork, err := getWork() + if err != nil { + work.errCh <- err + } else { + close(work.errCh) + work.resCh <- miningWork } + case result := <-ethash.submitWorkCh: + // Verify submitted PoW solution based on maintained mining blocks. + if submitWork(result.nonce, result.mixDigest, result.hash) { + close(result.errCh) + } else { + result.errCh <- errInvalidSealResult + } + + case result := <-ethash.submitRateCh: + // Trace remote sealer's hash rate by submitted value. + rates[result.id] = hashrate{rate: result.rate, ping: time.Now()} + close(result.done) + + case req := <-ethash.fetchRateCh: + // Gather all hash rate submitted by remote sealer. + var total uint64 + for _, rate := range rates { + // this could overflow + total += rate.rate + } + req <- total + case <-ticker.C: - // Clear stale submited hashrate - for id, rate := range hashrate { + // Clear stale submitted hash rate. + for id, rate := range rates { if time.Since(rate.ping) > 10*time.Second { - delete(hashrate, id) + delete(rates, id) } } - case <-ethash.exitCh: - // Exit remote loop if ethash object is cleared by GC + case errCh := <-ethash.exitCh: + // Exit remote loop if ethash is closed and return relevant error. + errCh <- nil break running } } diff --git a/eth/backend.go b/eth/backend.go index a18abdfb56..62a85f3fcb 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -411,6 +411,7 @@ func (s *Ethereum) Start(srvr *p2p.Server) error { func (s *Ethereum) Stop() error { s.bloomIndexer.Close() s.blockchain.Stop() + s.engine.Close() s.protocolManager.Stop() if s.lesServer != nil { s.lesServer.Stop() @@ -421,6 +422,5 @@ func (s *Ethereum) Stop() error { s.chainDb.Close() close(s.shutdownChan) - return nil } diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 89ebceec7c..3364f41eb7 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -21,6 +21,7 @@ var Modules = map[string]string{ "admin": Admin_JS, "chequebook": Chequebook_JS, "clique": Clique_JS, + "ethash": Ethash_JS, "debug": Debug_JS, "eth": Eth_JS, "miner": Miner_JS, @@ -109,6 +110,29 @@ web3._extend({ }); ` +const Ethash_JS = ` +web3._extend({ + property: 'ethash', + methods: [ + new web3._extend.Method({ + name: 'getWork', + call: 'ethash_getWork', + params: 0 + }), + new web3._extend.Method({ + name: 'submitWork', + call: 'ethash_submitWork', + params: 3, + }), + new web3._extend.Method({ + name: 'submitHashRate', + call: 'ethash_submitHashRate', + params: 2, + }), + ] +}); +` + const Admin_JS = ` web3._extend({ property: 'admin', diff --git a/les/backend.go b/les/backend.go index 35f67f29f8..952d92cc2a 100644 --- a/les/backend.go +++ b/les/backend.go @@ -248,6 +248,7 @@ func (s *LightEthereum) Stop() error { s.blockchain.Stop() s.protocolManager.Stop() s.txPool.Stop() + s.engine.Close() s.eventMux.Stop() diff --git a/miner/worker.go b/miner/worker.go index 624be1d0e6..210d22d7e9 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -447,13 +447,6 @@ func (self *worker) commitNewWork() { if self.config.DAOForkSupport && self.config.DAOForkBlock != nil && self.config.DAOForkBlock.Cmp(header.Number) == 0 { misc.ApplyDAOHardFork(work.state) } - pending, err := self.eth.TxPool().Pending() - if err != nil { - log.Error("Failed to fetch pending transactions", "err", err) - return - } - txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending) - work.commitTransactions(self.mux, txs, self.chain, self.coinbase) // compute uncles for the new block. var ( @@ -477,16 +470,41 @@ func (self *worker) commitNewWork() { for _, hash := range badUncles { delete(self.possibleUncles, hash) } - // Create the new block to seal with the consensus engine + + // Create an empty block based on temporary copied state for sealing in advance without waiting block + // execution finished. + if work.Block, err = self.engine.Finalize(self.chain, header, work.state.Copy(), nil, uncles, nil); err != nil { + log.Error("Failed to finalize block for temporary sealing", "err", err) + } else { + // Push empty work in advance without applying pending transaction. + // The reason is transactions execution can cost a lot and sealer need to + // take advantage of this part time. + if atomic.LoadInt32(&self.mining) == 1 { + log.Info("Commit new empty mining work", "number", work.Block.Number(), "uncles", len(uncles)) + } + self.push(work) + } + + // Fill the block with all available pending transactions. + pending, err := self.eth.TxPool().Pending() + if err != nil { + log.Error("Failed to fetch pending transactions", "err", err) + return + } + txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending) + work.commitTransactions(self.mux, txs, self.chain, self.coinbase) + + // Create the full block to seal with the consensus engine if work.Block, err = self.engine.Finalize(self.chain, header, work.state, work.txs, uncles, work.receipts); err != nil { log.Error("Failed to finalize block for sealing", "err", err) return } // We only care about logging if we're actually mining. if atomic.LoadInt32(&self.mining) == 1 { - log.Info("Commit new mining work", "number", work.Block.Number(), "txs", work.tcount, "uncles", len(uncles), "elapsed", common.PrettyDuration(time.Since(tstart))) + log.Info("Commit new full mining work", "number", work.Block.Number(), "txs", work.tcount, "uncles", len(uncles), "elapsed", common.PrettyDuration(time.Since(tstart))) self.unconfirmed.Shift(work.Block.NumberU64() - 1) } + // Push full work to sealer, which will replace the empty work sent before automatically. self.push(work) self.updateSnapshot() }