diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 1f6f2bb295..4d7bcc65f1 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -108,6 +108,9 @@ func init() { utils.MiningEnabledFlag, utils.AutoDAGFlag, utils.TargetGasLimitFlag, + utils.StrategyZerodiffEnabledFlag, + utils.StrategyZerodiffInstantTxsFlag, + utils.StrategyZerodiffMinEtherFlag, utils.NATFlag, utils.NoDiscoverFlag, utils.DiscoveryV5Flag, diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 9349857e91..c35f47898c 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -138,6 +138,14 @@ var AppHelpFlagGroups = []flagGroup{ utils.ExtraDataFlag, }, }, + { + Name: "MINER STRATEGY", + Flags: []cli.Flag{ + utils.StrategyZerodiffEnabledFlag, + utils.StrategyZerodiffInstantTxsFlag, + utils.StrategyZerodiffMinEtherFlag, + }, + }, { Name: "GAS PRICE ORACLE", Flags: []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 04db9a16f3..a4d10c2c2b 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -43,6 +43,7 @@ import ( "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discv5" @@ -202,6 +203,19 @@ var ( Name: "extradata", Usage: "Block extra data set by the miner (default = client version)", } + StrategyZerodiffEnabledFlag = cli.BoolFlag{ + Name: "miner.zerodiff", + Usage: "Enables the base zero-diff miner strategy (private networks)", + } + StrategyZerodiffInstantTxsFlag = cli.BoolFlag{ + Name: "miner.zerodiff.instanttxs", + Usage: "Ignores zero-diff delays if block contains transactions (private networks)", + } + StrategyZerodiffMinEtherFlag = cli.IntFlag{ + Name: "miner.zerodiff.minether", + Usage: "Ignores zero-diff delays if miner balance is below threshold (private networks)", + Value: 0, + } // Account settings UnlockedAccountFlag = cli.StringFlag{ Name: "unlock", @@ -636,6 +650,24 @@ func MakeMinerExtra(extra []byte, ctx *cli.Context) []byte { return extra } +// MakeMinerStrategies assembles any miner strategies from the set command line +// flags. +func MakeMinerStrategies(ctx *cli.Context) []*miner.Strategy { + var strategies []*miner.Strategy + + // If zero-diff mining is requested, assemble it + var ( + zerodiffBaseline = ctx.GlobalBool(StrategyZerodiffEnabledFlag.Name) + zerodiffInstanttxs = ctx.GlobalBool(StrategyZerodiffInstantTxsFlag.Name) + zerodiffThreshold = ctx.GlobalInt(StrategyZerodiffMinEtherFlag.Name) + ) + if zerodiffBaseline || zerodiffInstanttxs || zerodiffThreshold > 0 { + threshold := new(big.Int).Mul(big.NewInt(int64(zerodiffThreshold)), common.Ether) + strategies = append(strategies, miner.NewZeroDiffStrategy(zerodiffInstanttxs, threshold)) + } + return strategies +} + // MakePasswordList reads password lines from the file specified by --password. func MakePasswordList(ctx *cli.Context) []string { path := ctx.GlobalString(PasswordFileFlag.Name) @@ -742,6 +774,7 @@ func RegisterEthService(ctx *cli.Context, stack *node.Node, extra []byte) { DatabaseHandles: MakeDatabaseHandles(), NetworkId: ctx.GlobalInt(NetworkIdFlag.Name), MinerThreads: ctx.GlobalInt(MinerThreadsFlag.Name), + MinerStrategies: MakeMinerStrategies(ctx), ExtraData: MakeMinerExtra(extra, ctx), DocRoot: ctx.GlobalString(DocRootFlag.Name), GasPrice: GlobalBig(ctx, GasPriceFlag.Name), diff --git a/eth/backend.go b/eth/backend.go index 5a8b41f5d2..4afc47bccf 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -84,10 +84,11 @@ type Config struct { PowShared bool ExtraData []byte - Etherbase common.Address - GasPrice *big.Int - MinerThreads int - SolcPath string + Etherbase common.Address + GasPrice *big.Int + MinerThreads int + MinerStrategies []*miner.Strategy + SolcPath string GpoMinGasPrice *big.Int GpoMaxGasPrice *big.Int @@ -233,7 +234,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { if eth.protocolManager, err = NewProtocolManager(eth.chainConfig, config.FastSync, config.NetworkId, maxPeers, eth.eventMux, eth.txPool, eth.pow, eth.blockchain, chainDb); err != nil { return nil, err } - eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.pow) + eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.pow, config.MinerStrategies) eth.miner.SetGasPrice(config.GasPrice) eth.miner.SetExtra(config.ExtraData) diff --git a/miner/miner.go b/miner/miner.go index 33d77e174d..a6c7b71bcf 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -59,12 +59,12 @@ type Miner struct { shouldStart int32 // should start indicates whether we should start after sync } -func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, pow pow.PoW) *Miner { +func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, pow pow.PoW, strategies []*Strategy) *Miner { miner := &Miner{ eth: eth, mux: mux, pow: pow, - worker: newWorker(config, common.Address{}, eth, mux), + worker: newWorker(config, common.Address{}, eth, mux, strategies), canStart: 1, } go miner.update() diff --git a/miner/strategy.go b/miner/strategy.go new file mode 100644 index 0000000000..de0aa410af --- /dev/null +++ b/miner/strategy.go @@ -0,0 +1,157 @@ +// Copyright 2016 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" + "math/rand" + "time" + + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// zerodiffBlocktime block time for the zero-diff miner to target. Note, this +// needs to be in sync with the difficulty adjustment algorithm so chaning this +// or making it configurable doesn't make sense. +const zerodiffBlocktime = 15 * time.Second + +// Strategy is a collection of optional callback methods that miner strategies +// may implement to influence the behavior of the miner. +type Strategy struct { + Name string // Name of the strategy for logging purposes + + // OnNewWork is invoked when a new block is starting to be mined by the miner. + OnNewWork func(emux *event.TypeMux, chain *core.BlockChain, block *types.Block) error + + // OnMinedBlock is invoked when a new block is mined by the miner. + OnMinedBlock func(emux *event.TypeMux, chain *core.BlockChain, pool *core.TxPool, block *types.Block) error +} + +// NewZeroDiffStrategy creates a mining strategy that aims to push the difficulty +// of the blocks down to zero. It tries to achive this by delaying found blocks +// to always be above the configured block time limit, pushing the difficulty +// down a bit after every block. Similarly if it notices the difficulty going up, +// it aborts mining altogether. +// +// The strategy has a few nice properties that is orthogonal both to multiple +// zero-diff miners as well as other plain miners: +// +// * The zero-diff miner may be ran with arbitrarilly many threads: if a block +// is found faster than the target block time, it will be delayed anyway, so +// the mining threads will go idle. If on the other hand the difficulty is +// pushed up by a rouge miner and abandoned, the multiple threads will allow +// pulling the difficulty down faster until it reaches sub-target times. +// * The zero-diff miner can play along nicely with non zero-diff miners, since +// it will either delay its block to above-target times, or outright discard +// its own block if another is found, thereby ensuring it only ever reduces +// the difficulty, never increases. +// * The zero-diff miner can also play along nicely with other zero-diff miners +// by simulating block times at random between [target, 1.1*target], allowing +// multiple zero-diff miners to co-exist and share blocks, without racing each +// other for blocks and leading to a high uncle rate. +// +// There are two short-circuits built in that can cause the zero-diff miner to +// temporarilly turn off and mine at full capacity: +// +// * If instant-transactions are requested, then blocks containing transactions +// will not be delayed at all; furthermore empty blocks will be fast-tracked +// too if the transaction pool contains pending transactions ready to be added +// to the next block. +// * If a minimum balance threshold is set for the miner, no delays will come +// into effect until that minimum threshold is reached. +// +// Note, this strategy is only meaningful in trusted private networks where the +// goal of mining is not to secure the network, rather to provide a stable but +// resource-light testbed. +func NewZeroDiffStrategy(instantTxs bool, minBalance *big.Int) *Strategy { + return &Strategy{ + Name: "zero-diff", + + OnNewWork: func(emux *event.TypeMux, chain *core.BlockChain, block *types.Block) error { + // If the difficulty is increasing, give a change for others to waste resources + if parent := chain.GetBlock(block.ParentHash(), block.NumberU64()-1); parent.Difficulty().Cmp(block.Difficulty()) < 0 { + // Before bailing out, do check if balance threshold needs to be reached + if minBalance != nil { + if state, _ := chain.State(); state.GetBalance(block.Coinbase()).Cmp(minBalance) < 0 { + return nil + } + } + // Someone seems to be mining, listen for events though to avoid hangs if the other leaves + head := emux.Subscribe(core.ChainHeadEvent{}) + defer head.Unsubscribe() + + if chain.CurrentHeader().Hash() != block.ParentHash() { + return errors.New("stale work") // Another block arrived already, drop this + } + select { + case <-time.After(zerodiffBlocktime): + return nil // Ok, nobody else seems to be mining, fire up the CPU + case <-head.Chan(): + return errors.New("concurrent miner") // Meh, someone's mining, let them burn + } + } + return nil + }, + + OnMinedBlock: func(emux *event.TypeMux, chain *core.BlockChain, pool *core.TxPool, block *types.Block) error { + // A new block was mined, calculate the required delay + elapsed := time.Since(time.Unix(block.Time().Int64(), 0)) + delay := float64(zerodiffBlocktime)*(1.0+rand.Float64()/10.0) - float64(elapsed) + + // If the delay is negative, block times are way over the target already, release + if delay <= 0 { + return nil + } + // If instant transactions are enabled and the block contains something, release + if instantTxs && len(block.Transactions()) > 0 { + return nil + } + // If minimum balance requirements are set but not met, release + if minBalance != nil { + if state, _ := chain.State(); state.GetBalance(block.Coinbase()).Cmp(minBalance) < 0 { + return nil + } + } + // Otherwise monitor head and transaction events + head := emux.Subscribe(core.ChainHeadEvent{}) + defer head.Unsubscribe() + + txs := emux.Subscribe(core.TxPreEvent{}) + defer txs.Unsubscribe() + + // Double check for events that happened before subscriptions + if chain.CurrentHeader().Hash() != block.ParentHash() { + return errors.New("stale block") // Another block arrived already, drop this + } + if pend, _ := pool.Stats(); pend > 0 { + return nil // Transaction pending in the pool, release this and start processing them + } + // Wait for some event to occur that releases or bins the block + select { + case <-time.After(time.Duration(delay)): + return nil // Timeout for difficulty reduction reached, release + case <-head.Chan(): + return errors.New("concurrent block") // Alternate block arrived, drop this + case <-txs.Chan(): + return nil // Transaction arrived, include it as fast as possible + } + }, + } +} diff --git a/miner/worker.go b/miner/worker.go index 2f090924e2..bd40202a65 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -114,7 +114,8 @@ type worker struct { txQueueMu sync.Mutex txQueue map[common.Hash]*types.Transaction - unconfirmed *unconfirmedBlocks // set of locally mined blocks pending canonicalness confirmations + unconfirmed *unconfirmedBlocks // Set of locally mined blocks pending confirmation + strategies []*Strategy // Miner strategies currently ran by the miner // atomic status counters mining int32 @@ -123,7 +124,7 @@ type worker struct { fullValidation bool } -func newWorker(config *params.ChainConfig, coinbase common.Address, eth Backend, mux *event.TypeMux) *worker { +func newWorker(config *params.ChainConfig, coinbase common.Address, eth Backend, mux *event.TypeMux, strategies []*Strategy) *worker { worker := &worker{ config: config, eth: eth, @@ -138,6 +139,7 @@ func newWorker(config *params.ChainConfig, coinbase common.Address, eth Backend, txQueue: make(map[common.Hash]*types.Transaction), agents: make(map[Agent]struct{}), unconfirmed: newUnconfirmedBlocks(eth.BlockChain(), 5), + strategies: strategies, fullValidation: false, } worker.events = worker.mux.Subscribe(core.ChainHeadEvent{}, core.ChainSideEvent{}, core.TxPreEvent{}) @@ -275,6 +277,22 @@ func (self *worker) wait() { block := result.Block work := result.Work + // Run any miner strategies interested in block mine results and ensure none denies this block + denied := false + for _, strat := range self.strategies { + if strat.OnMinedBlock != nil { + glog.V(logger.Debug).Infof("Passing mined block #%d [%x…] to strategy %s", block.Number(), block.Hash().Bytes()[:4], strat.Name) + if err := strat.OnMinedBlock(self.mux, self.chain, self.eth.TxPool(), block); err != nil { + glog.V(logger.Debug).Infof("Mined block #%d [%x…] denied by strategy %s: %v", block.Number(), block.Hash().Bytes()[:4], strat.Name, err) + denied = true + break + } + glog.V(logger.Debug).Infof("Mined block #%d [%x…] accepted by strategy %s", block.Number(), block.Hash().Bytes()[:4], strat.Name) + } + } + if denied { + continue + } if self.fullValidation { if _, err := self.chain.InsertChain(types.Blocks{block}); err != nil { log.Error(fmt.Sprint("mining err", err)) @@ -349,9 +367,22 @@ func (self *worker) wait() { // push sends a new work task to currently live miner agents. func (self *worker) push(work *Work) { + // If there are no agents mining, bail out if atomic.LoadInt32(&self.mining) != 1 { return } + // Iterate over all miner strategies and ensure none denies this block + for _, strat := range self.strategies { + if strat.OnMinedBlock != nil { + glog.V(logger.Debug).Infof("Passing mining request #%d [%x…] to strategy %s", work.Block.Number(), work.Block.Hash().Bytes()[:4], strat.Name) + if err := strat.OnNewWork(self.mux, self.chain, work.Block); err != nil { + glog.V(logger.Debug).Infof("Mining request #%d [%x…] denied by strategy %s: %v", work.Block.Number(), work.Block.Hash().Bytes()[:4], strat.Name, err) + return + } + glog.V(logger.Debug).Infof("Mining request #%d [%x…] accepted by strategy %s", work.Block.Number(), work.Block.Hash().Bytes()[:4], strat.Name) + } + } + // Block accepted for mining, proceed for agent := range self.agents { atomic.AddInt32(&self.atWork, 1) if ch := agent.Work(); ch != nil { @@ -515,7 +546,7 @@ func (self *worker) commitNewWork() { log.Info(fmt.Sprintf("commit new work on block %v with %d txs & %d uncles. Took %v\n", work.Block.Number(), work.tcount, len(uncles), time.Since(tstart))) self.unconfirmed.Shift(work.Block.NumberU64() - 1) } - self.push(work) + go self.push(work) } func (self *worker) commitUncle(work *Work, uncle *types.Header) error {