PoW 挖矿在区块链领域是备受关注的。它到底是怎样运作的?这里面蕴含着很多内容。

PoW挖矿的基础数据结构

PoW 挖矿拥有极为重要的基础数据结构。知晓这些结构是踏入挖矿世界的首要步骤。比如,在研习以太坊挖矿之前,有与之相关的数据结构作为基础。同时,对于 block 的数据结构也需相当熟悉。在实际代码的.go 文件中进行 New 操作时,会涉及特定语句的调用,这些都是构建 PoW 挖矿复杂体系的基础要素。在 2019 年,相关的体系研究就已经开始进行,当时致力于梳理这些繁杂的基础结构。这些基础数据结构并非凭空产生,而是众多开发者不断探索的成果。

数据结构1:
type Miner struct {
    mux *event.TypeMux // 事件锁,已被feed.mu.lock替代
    worker *worker // 干活的人
    coinbase common.Address // 结点地址
    mining   int32 // 代表挖矿进行中的状态
    eth      Backend // Backend对象,Backend是一个自定义接口封装了所有挖矿所需方法。
    engine   consensus.Engine // 共识引擎
    canStart    int32 // 是否能够开始挖矿操作
    shouldStart int32 // 同步以后是否应该开始挖矿
}
//实际的工人
type worker struct {
	config *params.ChainConfig //链配置
	engine consensus.Engine //一致性引擎,ethash或者clique poa(这个目前只在测试网测试)
	mu sync.Mutex //锁
	// update loop
	mux    *event.TypeMux 
	events *event.TypeMuxSubscription
	wg     sync.WaitGroup
	agents map[Agent]struct{} //agent 是挖矿代理,实际执行挖矿的代理,目前以太坊默认注册cpuagent,矿池应该是自己实现了自己的agent注册到这里
	recv   chan *Result //这是一个结果通道,挖矿完成以后将结果推送到此通道
	eth     Backend //以太坊定义
	chain   *core.BlockChain
	proc    core.Validator
	chainDb ethdb.Database
	coinbase common.Address //基础帐户地址
	extra    []byte
	currentMu sync.Mutex
	current   *Work  //实际将每一个区块作为一个工作work推给agent进行挖矿
	uncleMu        sync.Mutex
	possibleUncles map[common.Hash]*types.Block //可能的数块
	txQueueMu sync.Mutex
	txQueue   map[common.Hash]*types.Transaction
	unconfirmed *unconfirmedBlocks // set of locally mined blocks pending canonicalness confirmations
	// atomic status counters
	mining int32
	atWork int32
	fullValidation bool
}
//agent接口如下,实现以下接口的 就可作为一个agent
type Agent interface {
	Work() chan<- *Work
	SetReturnCh(chan<- *Result)
	Stop()
	Start()
	GetHashRate() int64
}

同时,不同的数据结构有着不同的作用。它们一起协作,就如同众多细微的齿轮一般,推动着整个 PoW 挖矿系统进行运转。只有对这些数据结构进行深入的研究,才能够对后续的挖矿流程有更加清晰透彻的理解。

事件触发与反应

挖矿过程中的一些事件及其反应很值得去探讨。它就如同一个精密的仪器,各个功能之间存在着关联。第一个事件是在区块链里加入了一个新的区块作为整个链的链头,此时的反应是马上开始准备挖掘下一个新区块,这个过程一直都在进行,没有停歇。另外,当在区块链中加入一个新的区块作为当前链头的旁支时,这个区块会被收纳进[]数组,成为下一个挖掘新区块可能的 Uncles 之一,为后续的挖掘提供了不同的可能路径。

//先单独看如下两句:
	engine: CreateConsensusEngine(ctx, config, chainConfig, chainDb),
	engine := ethash.New(ctx.ResolvePath(config.EthashCacheDir), config.EthashCachesInMem, config.EthashCachesOnDisk,config.EthashDatasetDir, config.EthashDatasetsInMem, config.EthashDatasetsOnDisk)
   //从上面可以看出来geth启动时候默认的共识引擎为ethash
 //下面语句开始New一个miner了
eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)
 
 # go-ethereum/miner/miner.go
func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine) *Miner {
	//开始创建miner结构体
	miner := &Miner{
		eth:      eth,
		mux:      mux,
		engine:   engine,
		worker:   newWorker(config, engine, common.Address{}, eth, mux), //创建了一个工人
		canStart: 1,
	}
	//注册代理
	miner.Register(NewCpuAgent(eth.BlockChain(), engine))
	go miner.update()
	return miner
}
# go-ethereum/miner/worker.go
func newWorker(config *params.ChainConfig, engine consensus.Engine, coinbase common.Address, eth Backend, mux *event.TypeMux) *worker {
	worker := &worker{
		config:         config,
		engine:         engine,
		eth:            eth,
		mux:            mux,
		chainDb:        eth.ChainDb(),
		recv:           make(chan *Result, resultQueueSize), //结果通道
		chain:          eth.BlockChain(),
		proc:           eth.BlockChain().Validator(),
		possibleUncles: make(map[common.Hash]*types.Block),
		coinbase:       coinbase,
		txQueue:        make(map[common.Hash]*types.Transaction),
		agents:         make(map[Agent]struct{}),
		unconfirmed:    newUnconfirmedBlocks(eth.BlockChain(), 5),
		fullValidation: false,
	}
	//worker开始订阅相关三个事件
	worker.events = worker.mux.Subscribe(core.ChainHeadEvent{}, core.ChainSideEvent{}, core.TxPreEvent{})
	//先来分析一下update()函数
	go worker.update()
	go worker.wait()
	worker.commitNewWork()
	return worker
}
func (self *worker) update() {
	//遍历自己的事件通道
	for event := range self.events.Chan() {
		// A real event arrived, process interesting content
		switch ev := event.Data.(type) {
		//如果是新区块加入事件,那么工人开始挖下一个区块
		case core.ChainHeadEvent:
			self.commitNewWork()
		//如果是区块旁支事件(俗称的叔块)
		case core.ChainSideEvent:
			self.uncleMu.Lock()
			//在map结构里添加可能的叔块
			self.possibleUncles[ev.Block.Hash()] = ev.Block
			self.uncleMu.Unlock()
		case core.TxPreEvent:
			// Apply transaction to the pending state if we're not mining
			if atomic.LoadInt32(&self.mining) == 0 {
				self.currentMu.Lock()
				acc, _ := types.Sender(self.current.signer, ev.Tx)
				txs := map[common.Address]types.Transactions{acc: {ev.Tx}}
				txset := types.NewTransactionsByPriceAndNonce(txs)
				self.current.commitTransactions(self.mux, txset, self.chain, self.coinbase)
				self.currentMu.Unlock()
			}
		}
	}
}

另一个事件与新的交易 tx 被加入相关。若在未处于挖掘状态时,执行该 tx 并将其收纳进 Work.txs 数组,以作下次挖掘新区块的备用。需注意,不一定是由外部源发出。挖矿体系内的事件相互关联,这些事件的反应机制保障了挖矿过程的有序进行。

从设计角度看挖矿结构体

挖矿结构体即 Miner,在挖矿过程中起着核心架构的作用。它组合了诸多元素,这些元素的关系如同建筑里的梁与柱,彼此相互支撑。在创建 New Miner 时,首先要进行一些预先的操作,其中包含去订阅相关事件。这一操作仿佛给挖矿结构体赋予了“耳朵”,使其能够听到挖矿过程中的各种“声音”。

这个结构体开启了两个主要线程,分别是 go.() 和 go.wait(),它们也是很有意义的。其中前面一个线程主要是遍历事件并做出相应动作,而后面这个线程负责将一个新的区块填充好,完成交易执行,发送奖励等一系列操作。这两个线程就如同两只巧手,精心地处理着挖矿过程中的各项事务。

共识引擎在挖矿中的作用

共识引擎在 PoW 挖矿中有着极为重要的地位。它犹如挖矿界的裁判。在整个挖矿流程里,拿到 work 之后就会调用共识引擎的 Seal 方法来进行共识计算。此过程对外是向整个网络进行宣告,对内则是进行挖矿的内部运算。整个计算的目的是找到一个恰当的 nonce,这就像是在寻找一把能打开宝藏的钥匙。

当计算找到这个合适的 nonce 时,结果会被提交到一个结果通道。通过共识引擎的这一操作,能够保障比特币等加密货币挖矿结果在整个网络中的权威性与一致性。每一个挖矿的结果都必须经过它的判定,才能够被认可。

commitNewWork()在另外一篇文章中已经单独分析,接下来主要分析worker.wait()
func (self *worker) wait() {
	for {
		mustCommitNewWork := true
         //worker.wait会一直阻塞在这里,等待有新的区块经过seal后被推送到recv通道
		for result := range self.recv {
			atomic.AddInt32(&self.atWork, -1)
			if result == nil {
				continue
			}
			block := result.Block
			work := result.Work
			//是否是全验证模式
			if self.fullValidation {
				//将新区块插入到主链
				if _, err := self.chain.InsertChain(types.Blocks{block}); err != nil {
					log.Error("Mined invalid block", "err", err)
					continue
				}
                //发送新挖出区块事件,会通知当前的miner和protocolManager和其他订阅者
				go self.mux.Post(core.NewMinedBlockEvent{Block: block})
			} else {
				work.state.CommitTo(self.chainDb, self.config.IsEIP158(block.Number()))
				stat, err := self.chain.WriteBlock(block)
				if err != nil {
					log.Error("Failed writing block to chain", "err", err)
					continue
				}
				// update block hash since it is now available and not when the receipt/log of individual transactions were created
                //遍历当前所挖区块的所有txreceipts,给log的blockhash字段填充值
				for _, r := range work.receipts {
					for _, l := range r.Logs {
						l.BlockHash = block.Hash()
					}
				}
				for _, log := range work.state.Logs() {
					log.BlockHash = block.Hash()
				}
				// check if canon block and write transactions
				if stat == core.CanonStatTy {
					// This puts transactions in a extra db for rpc
					core.WriteTransactions(self.chainDb, block)
					// store the receipts
					core.WriteReceipts(self.chainDb, work.receipts)
					// Write map map bloom filters
					core.WriteMipmapBloom(self.chainDb, block.NumberU64(), work.receipts)
					// implicit by posting ChainHeadEvent
					mustCommitNewWork = false
				}
				//广播相关事件出去
				// broadcast before waiting for validation
				go func(block *types.Block, logs []*types.Log, receipts []*types.Receipt) {
					self.mux.Post(core.NewMinedBlockEvent{Block: block})
					self.mux.Post(core.ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})
					if stat == core.CanonStatTy {
						self.mux.Post(core.ChainHeadEvent{Block: block})
						self.mux.Post(logs)
					}
					if err := core.WriteBlockReceipts(self.chainDb, block.Hash(), block.NumberU64(), receipts); err != nil {
						log.Warn("Failed writing block receipts", "err", err)
					}
				}(block, work.state.Logs(), work.receipts)
			}
            //将区块号和区块hash插入未确认表
			// Insert the block into the set of pending ones to wait for confirmations
			self.unconfirmed.Insert(block.NumberU64(), block.Hash())
			//如果再挖出一个新块必须开启下一次挖掘工作,那么执行新的挖矿工作
			if mustCommitNewWork {
				self.commitNewWork()
			}
		}
	}
}
接下来回到Miner.New下面继续看miner.Register(NewCpuAgent(eth.BlockChain(), engine))
    
func (self *Miner) Register(agent Agent) {
    //如果自己开启了挖矿
	if self.Mining() {
        //那么启动代理
		agent.Start()
	}
    //在工人处注册此代理
	self.worker.register(agent)
}
# go-ethereum/miner/agent.go
func (self *CpuAgent) Start() {
	//类似java的CAS
	if !atomic.CompareAndSwapInt32(&self.isMining, 0, 1) {
		return // agent already started
	}
	//自己开启一个携程进行工作
	go self.update()
}
func (self *CpuAgent) update() {
out:
	//死循环工作,以太坊常常做的事情,哈哈
	for {
		select {
		//遍历workCh,查看是否有work提交,前面分析commitNewWork()时候讲解到会将一个区块信息填充执行Finalize后提交到此通道
		case work := <-self.workCh:
			self.mu.Lock()
			if self.quitCurrentOp != nil {
				close(self.quitCurrentOp)
			}
			self.quitCurrentOp = make(chan struct{})
			//开启协程执行挖矿操作,核心操作
			go self.mine(work, self.quitCurrentOp)
			self.mu.Unlock()
		case <-self.stop:
			self.mu.Lock()
			if self.quitCurrentOp != nil {
				close(self.quitCurrentOp)
				self.quitCurrentOp = nil
			}
			self.mu.Unlock()
			break out
		}
	}
	............................
}
func (self *CpuAgent) mine(work *Work, stop <-chan struct{}) {
	//实际调用ethash.Seal进行挖矿
	if result, err := self.engine.Seal(self.chain, work.Block, stop); result != nil {
		log.Info("Successfully sealed new block", "number", result.Number(), "hash", result.Hash())
		//如果挖矿有结果则推送到returnCh,交给worker.wait()处理
		self.returnCh <- &Result{work, result}
	} else {
		if err != nil {
			log.Warn("Block sealing failed", "err", err)
		}
		self.returnCh <- nil
	}
}
# go-ethereum/consensus/ethhash/sealer.go
// Seal implements consensus.Engine, attempting to find a nonce that satisfies
// the block's difficulty requirements.
func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {
	//一种测试模式
	// If we're running a fake PoW, simply return a 0 nonce immediately
	if ethash.fakeMode {
		header := block.Header()
		header.Nonce, header.MixDigest = types.BlockNonce{}, common.Hash{}
		return block.WithSeal(header), nil
	}
	//一种测试模式
	// If we're running a shared PoW, delegate sealing to it
	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{})
	//结果通道
	found := make(chan *types.Block)
	ethash.lock.Lock()
	threads := ethash.threads
	//开始为区块中的nonce做准备
	if ethash.rand == nil {
		//使用"crypto/rand"下的函数生成随机数种子
		seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
		if err != nil {
			ethash.lock.Unlock()
			return nil, err
		}
		//使用随机数种子生成随机数赋值给ethash.hash
		ethash.rand = rand.New(rand.NewSource(seed.Int64()))
	}
	ethash.lock.Unlock()
	//如果挖矿线程数为0则将线程数赋值为cpu个数
	if threads == 0 {
		threads = runtime.NumCPU()
	}
	if threads < 0 {
		threads = 0 // Allows disabling local mining without extra logic around local/remote
	}
	var pend sync.WaitGroup
	//开启数个线程同时执行挖矿
	for i := 0; i < threads; i++ {
		pend.Add(1)
		go func(id int, nonce uint64) {
			defer pend.Done()
			ethash.mine(block, id, nonce, abort, found) //调用ethash进行实际挖矿
		}(i, uint64(ethash.rand.Int63())) //将上面生成的随机数赋值给nonce做初始值
	}
	//一直在此等着上面有如下几种结果之一出现
	// Wait until sealing is terminated or a nonce is found
	var result *types.Block
	select {
	case <-stop:
		// Outside abort, stop all miner threads
		close(abort)
	case result = <-found:
		// One of the threads found a block, abort all others
		close(abort)
	case <-ethash.update:
		// Thread count was changed on user request, restart
		close(abort)
		pend.Wait()
		return ethash.Seal(chain, block, stop)
	}
	// Wait for all miners to terminate and return the block
	pend.Wait()
	return result, nil
}
//实际的挖矿函数
// mine is the actual proof-of-work miner that searches for a nonce starting from
// seed that results in correct final block difficulty.
func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
	// Extract some data from the header
	var (
		header = block.Header() 
		hash   = header.HashNoNonce().Bytes() //获取commitNewWork提交来的区块头无nonce的hash
		target = new(big.Int).Div(maxUint256, header.Difficulty) //target
		number  = header.Number.Uint64()
		dataset = ethash.dataset(number) //根据区块号获取数据集,数据集又是另一个话题
	)
	// Start generating random nonces until we abort or find a good one
	var (
		//尝试次数
		attempts = int64(0)
		//nonce
		nonce    = seed
	)
	logger := log.New("miner", id)
	logger.Trace("Started ethash search for new nonces", "seed", seed)
	for {
		select {
		case <-abort:
			// Mining terminated, update stats and abort
			logger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)
			ethash.hashrate.Mark(attempts)
			return
		default:
			// We don't have to update hash rate on every nonce, so update after after 2^X nonces
			attempts++
			//当尝试测试达到2的15次方时候,做一次标记,并从头开始
			if (attempts % (1 << 15)) == 0 {
				ethash.hashrate.Mark(attempts)
				attempts = 0
			}
			// Compute the PoW value of this nonce
			//下面就是主要计算符合挖矿条件的函数
			digest, result := hashimotoFull(dataset, hash, nonce)
			//如果计算结果比目标值小,那么就算挖矿成功
			if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
				// Correct nonce found, create a new header with it
				//拷贝区块头
				header = types.CopyHeader(header)
				//给区块头填充nonce值
				header.Nonce = types.EncodeNonce(nonce)
				//给区块mixHash字段填充值,为了验证做准备
				header.MixDigest = common.BytesToHash(digest)
				// Seal and return a block (if still needed)
				select {
				//将组装的block推送到found通道,其实最终交由worker.wait()处理
				case found <- block.WithSeal(header):
					logger.Trace("Ethash nonce found and reported", "attempts", nonce-seed, "nonce", nonce)
				case <-abort:
					logger.Trace("Ethash nonce found but discarded", "attempts", nonce-seed, "nonce", nonce)
				}
				return
			}
			//nonce在初始化以后每次都会自增一后重新尝试
			nonce++
		}
	}
}
//最后分析一下Miner.New的最后一个方法
// update keeps track of the downloader events. Please be aware that this is a one shot type of update loop.
// It's entered once and as soon as `Done` or `Failed` has been broadcasted the events are unregistered and
// the loop is exited. This to prevent a major security vuln where external parties can DOS you with blocks
// and halt your mining operation for as long as the DOS continues.
func (self *Miner) update() {
	//订阅download下的事件
	events := self.mux.Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{})
out:
	for ev := range events.Chan() {
		switch ev.Data.(type) {
		//如果downloader开始事件
		case downloader.StartEvent:
			//一个downloader开始,意味着需要去别的节点主动下载一些数据,那么理论上跟本地挖矿是冲突的,所以当一个downloader开始时候  将停止自己的挖矿
			atomic.StoreInt32(&self.canStart, 0)
			if self.Mining() {
				self.Stop()
				atomic.StoreInt32(&self.shouldStart, 1)
				log.Info("Mining aborted due to sync")
			}
			//如果downloader 完成事件,失败事件  都会在此开启挖矿
		case downloader.DoneEvent, downloader.FailedEvent:
			shouldStart := atomic.LoadInt32(&self.shouldStart) == 1
			atomic.StoreInt32(&self.canStart, 1)
			atomic.StoreInt32(&self.shouldStart, 0)
			if shouldStart {
				self.Start(self.coinbase)
			}
			// unsubscribe. we're only interested in this event once
			events.Unsubscribe()
			// stop immediately and ignore all further pending events
			break out
		}
	}
}

挖矿中的奖励分配

在 PoW 挖矿中,奖励分配是极为重要的一个环节。矿工挖到矿后,就能够获得相应的奖励。这份奖励如同给挖矿者们的一份馈赠,激励着他们投入大量的算力。并且,不仅仅是在挖到新的区块时会有奖励,挖到叔块时也会将奖励发送至相应的地址。奖励分配制度构建起了一整套的激励机制,使得整个挖矿社区变得活跃起来。

在这个奖励分配当中,时间、地点、人物等方面有所体现。像在一些规模较大的矿场里,众多的矿工都在进行竞争。在 2019 年那个时期,奖励分配机制已经较为成熟且完善,吸引了许多人投身到挖矿这一行业中。

挖矿整体流程总结

PoW 挖矿流程较为复杂。首先是基础数据结构方面,接着是事件触发,然后是挖矿结构体的运作,再到共识引擎的计算,最后是奖励的分配。这就如同一条长长的链条,其中任何一个环节都不可或缺。这些环节都是为了找到那个符合公式 rand(n,h)=M/D(n:nonce,h:,M:,D:)的 nonce 而存在的。

各个环节之间联系紧密,一旦有疏忽,就有可能引发挖矿失败或者效率变低的情况。这就需要众多矿工以及开发者持续对过程进行优化,以此来提升整个挖矿生态系统的效率和稳定性。

你是否了解你所在的地区有哪些比较出名的 PoW 挖矿企业或者团队?大家可以在评论区进行互动要是觉得这篇文章不错的话就点个赞并且分享一下。