主页 > imtoken下载app > 以太坊源码机制:挖矿【通俗易懂】
以太坊源码机制:挖矿【通俗易懂】
狗年吉祥,继续研究以太坊源码。 从本文开始,我们将深入以太坊的核心源码,进而对以太坊的核心技术进行分析和研究。 关键词:拜占庭,挖矿,矿工,分叉,源代码分析,叔块,代理,w
大家好,我是建筑先生,一个会写代码会吟诗的架构师。 今天说说以太坊的源码机制:挖矿,希望能帮助大家进步!!!
狗年吉祥,继续研究以太坊源码。 从本文开始,我们将深入以太坊的核心源码,进而对以太坊的核心技术进行分析和研究。
关键词:拜占庭,挖矿,矿工,分叉,源代码分析,叔块,代理,工人,事件监控
本文基于go-ethereum 1.7.3-stable源码版本。 源码范围主要在矿机pkg。
miner.start()
Miner是矿工的意思。 矿工要做的工作就是“挖矿”。 挖矿是将一系列最新的未封装交易封装到一个新区块中的过程。 在学习以太坊挖矿之前,我们首先要了解几个概念:
拜占庭将军问题
分布式系统的状态同步问题。
拜占庭帝国繁荣昌盛,周边几个小国的将领垂涎已久,但各有各的秘密。 他们一半以上的将领必须同意攻击拜占庭,不能在战场上背叛(达成共识),否则攻击会失败并烧毁自己。 而将军的领地,可能会被其他几位将军瓜分。 基于这种情况,将军之间的沟通就很成问题了。 有的人口是心非,有的人忠于组织利益。 如何最终达成共识是个问题。
分布式系统中的每个节点都是一个将军,当这些节点想要同步它们的状态时,就会面临拜占庭将军问题。 如何有效避免节点发送错误信息对结果的影响?
POW(工作量证明)
工作量证明,顾名思义,就是证明你做了多少工作。 POW是目前解决上述拜占庭一般问题最流行的共识算法。 比特币、以太坊等主流区块链数字货币均基于 POW。
为了解决拜占庭将军问题,首先需要确定一个方法:在这些平等的将军中选择一个忠诚的“大将军”,其他将军只能听从他的决定。
这似乎违背了去中心化的思想,但仔细分析,这些将军在做出这个决定之前都是去中心化的平等节点,被选中的将军只是为了这个决定。 决定重新选择。 未来几十年,他将不得不在朝廷内外服从他的命令,而不是以集中的方式固定一位将军。
想通了这个问题,接下来要解决的就是如何选将。 在决策之前,这些将军都是平等的节点,所以以他们在决策时的发言来判断。 将军们根据已知的战场信息,各自估计当前的战场形势,进行计算,得出结论(块)后广播给其他将军。 同时,将军必须时刻监听其他将军的广播内容。 一旦收到其他武将的结论广播,武将们就会立刻停止手中的算计,去验证广播的内容。 如果所有武将都通过了验证,那么第一个发出这个可验证结果广播的武将被选为武将,这次他决定听取他的结论(广播中已经包含了结果内容)。
所以在这个过程中有两个重要的因素:
首先是速度,第一个通过验证的可以被选为将军,第二个慢一步就没有机会了。 然后是正确性,即将军下的结论是否正确,是否能被其他将军成功验证。
速度的问题就是计算能力的问题。 比如我的电脑是8核32G的配置,计算能力肯定比你的单核1G内存还快。 这个和POW算法关系不大。POW算法是解决正确性的。 POW 提供了一种难以计算但易于验证的方法。 这是基于散列函数的特性。 在上一篇文章中提到,哈希函数是
通过这些特性,可以将工作量证明下发给每个带有哈希加密函数的节点,每个节点会计算出这个函数要封存的区块信息加上一个nonce值,得到一个加密后的哈希值。 这个加密的hash需要满足一定的规则(比如前四位必须是1111),每个节点只能通过穷举法继续尝试,直到满足条件,才会得出结论,即出块成功,然后对区块进行散列广播,其他节点验证确实满足预定规则(前四位确实是1111),则完成共识,由刚刚广播的节点产生区块。 这个工作量是指节点不断尝试计算的工作量。 得到符合条件的区块哈希后,经过广播,其他节点正在进行的、已经完成的工作量将被作废(其实这也是一种计算的浪费),证明该区块是你出的。
问题一:筛选块
也就是当你广播的时候,其他某个节点也计算出符合条件的哈希值,也就是该节点也产生一个相同编号的块。 这时候就需要比较两个区块的时间戳。 较早的一个将被确认并保留在链上,而另一个较晚的将被丢弃。
问题二:分叉链
当一个节点发布新的共识规则时,其他节点不同步该共识规则。 一般来说,新的共识规则是前向兼容的,即之前链上的数据仍然有效并被识别,但是没有同步新规则的节点会继续挖矿,挖出的区块将不会被识别或识别。由更新新规则的节点识别。 此时链分叉,分为1.0(旧共识规则)和2.0(新共识规则)两条链。 在这一点上,具有更大质量(矿工)基础的链将保留下来。
比如这条公链是我们公司发布的,我们会吸引更多的客户进来,但实际上作为发布者,这些客户的节点和我是平等的。 这时候只要有陌生节点加入,它也可以发布新的规则,而作为发布者以太坊可以挖吗,我们也需要更新我们的软件,所以社区非常重要。 通过社区,我们可以维护客户的支持和信任,我们在发布新规则时会得到他们的支持。 由于区块链本身的开源和去中心化的特性,我们的公链一旦发布,就不属于我们以太坊可以挖吗,而是属于每一个参与的节点,我们只有通过做实事来解决问题,才会得到客户矿工的认可保证本链优秀的竞争力(当然,作为发布者,我们有大量的预购币,所以对于链的发展,影响力的扩大,币的升值,我们更有实力) .
但是也有一种情况,就是有人还在用原来的1.0链,但是我想说的是,他们这个链的生命力肯定是在消亡,因为没有利益相关者,大家也不会免费付费。 区块链技术是平等公平的,大家得不到就不买单。
矿工源码分析
下面按照代码调试的顺序来分析以太坊miner.go文件的源代码内容。 整个以太坊挖矿相关的操作都是通过Miner结构暴露出来的方法:
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 // 同步以后是否应该开始挖矿
}
只听见山间传来建筑先生的声音:
夕阳欲落,花含烟,月明如素,忧愁无眠。 有谁会配上联或下联吗?
工人
Miner结构的其余部分已经介绍完了,但是worker对象还需要深入研究,因为外面有一个单独的worker.go文件,而Miner中包含了这个worker对象。 上面的评论给了“工人”。 每个矿工都会有一个worker成员对象,可以理解为一个worker,负责所有具体的挖矿工作流程。
此代码由Java架构师必看网-架构君整理type worker struct { config *params.ChainConfig engine consensus.Engine mu sync.Mutex // update loop mux *event.TypeMux txCh chan core.TxPreEvent txSub event.Subscription chainHeadCh chan core.ChainHeadEvent chainHeadSub event.Subscription chainSideCh chan core.ChainSideEvent chainSideSub event.Subscription wg sync.WaitGroup agents map[Agent]struct{} // worker拥有一个Agent的map集合 recv chan *Result eth Backend chain *core.BlockChain proc core.Validator chainDb ethdb.Database coinbase common.Address extra []byte currentMu sync.Mutex current *Work uncleMu sync.Mutex possibleUncles map[common.Hash]*types.Block unconfirmed *unconfirmedBlocks // 本地挖出的待确认的块 mining int32 atWork int32 }
矿工的属性非常多且具体,都与挖矿的具体操作有关,包括链本身的属性和区块数据结构的属性。 先看ChainConfig:
type ChainConfig struct {
ChainId *big.Int `json:"chainId"` // 链id标识了当前链,主键唯一id,也用于replay protection重发保护(用来防止replay attack重发攻击:恶意重复或拖延正确数据传输的一种网络攻击手段)
HomesteadBlock *big.Int `json:"homesteadBlock,omitempty"` // 当前链Homestead,置为0
DAOForkBlock *big.Int `json:"daoForkBlock,omitempty"` // TheDAO硬分叉切换。
DAOForkSupport bool `json:"daoForkSupport,omitempty"` // 结点是否支持或者反对DAO硬分叉。
// EIP150 implements the Gas price changes (https://github.com/ethereum/EIPs/issues/150)
EIP150Block *big.Int `json:"eip150Block,omitempty"` // EIP150 HF block (nil = no fork)
EIP150Hash common.Hash `json:"eip150Hash,omitempty"` // EIP150 HF hash (needed for header only clients as only gas pricing changed)
EIP155Block *big.Int `json:"eip155Block,omitempty"` // EIP155 HF block,没有硬分叉置为0
EIP158Block *big.Int `json:"eip158Block,omitempty"` // EIP158 HF block,没有硬分叉置为0
ByzantiumBlock *big.Int `json:"byzantiumBlock,omitempty"` // Byzantium switch block (nil = no fork, 0 = already on byzantium)
// Various consensus engines
Ethash *EthashConfig `json:"ethash,omitempty"`
Clique *CliqueConfig `json:"clique,omitempty"`
}
ChainConfig,顾名思义,就是链的配置属性。
Go语法补充:结构中的标签。 想必大家都对上面ChainId属性后面的``内容有疑惑,也就是结构体中的标签。 它是可选的,是变量的附加内容,可以通过reflect包读取。 通过观察ChainConfig结构体中的属性标签,可以看出这些标签在结构体转换中用于声明变量 是json结构体后的id值,可以与当前变量名不同。
言归正传,ChainConfig 包含了 ChainID 等属性,其中有很多是专门针对以太坊历史上出现的问题而配置的。
代理人
一个矿工有一个工人,一个工人有多个代理。 Agent接口定义在Worker.go文件中:
此代码由Java架构师必看网-架构君整理// Agent 可以注册到worker type Agent interface { Work() chan<- *Work SetReturnCh(chan<- *Result) Stop() Start() GetHashRate() int64 }
该接口有两种实现方式:CpuAgent 和 RemoteAgent。 这里使用了CpuAgent,由Agent来完成出块的工作。 同级的多个Agent之间存在竞争关系,最终通过共识算法完成出块工作。
type CpuAgent struct {
mu sync.Mutex // 锁
workCh chan *Work // Work通道对象
stop chan struct{} // 结构体通道对象
quitCurrentOp chan struct{} // 结构体通道对象
returnCh chan<- *Result // Result指针通道
chain consensus.ChainReader
engine consensus.Engine
isMining int32 // agent是否正在挖矿的标志位
}
挖矿start()全生命周期
要开始挖矿,首先要初始化一个矿工实例,
func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine) *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
}
创建矿工实例时,会根据Miner结构体的成员属性依次赋值。 红色的worker对象需要调用newWorker的构造函数。
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,
txCh: make(chan core.TxPreEvent, txChanSize),// TxPreEvent事件是TxPool发出的事件,代表一个新交易tx加入到了交易池中,这时候如果work空闲会将该笔交易收进work.txs,准备下一次打包进块。
chainHeadCh: make(chan core.ChainHeadEvent, chainHeadChanSize),// ChainHeadEvent事件,代表已经有一个块作为链头,此时work.update函数会监听到这个事件,则会继续挖新的区块。
chainSideCh: make(chan core.ChainSideEvent, chainSideChanSize),// ChainSideEvent事件,代表有一个新块作为链的旁支,会被放到possibleUncles数组中,可能称为叔块。
chainDb: eth.ChainDb(),// 区块链数据库
recv: make(chan *Result, resultQueueSize),
chain: eth.BlockChain(), // 链
proc: eth.BlockChain().Validator(),
possibleUncles: make(map[common.Hash]*types.Block),// 存放可能称为下一个块的叔块数组
coinbase: coinbase,
agents: make(map[Agent]struct{}),
unconfirmed: newUnconfirmedBlocks(eth.BlockChain(), miningLogAtDepth),// 返回一个数据结构,包括追踪当前未被确认的区块。
}
// 注册TxPreEvent事件到tx pool交易池
worker.txSub = eth.TxPool().SubscribeTxPreEvent(worker.txCh)
// 注册事件到blockchain
worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh)
worker.chainSideSub = eth.BlockChain().SubscribeChainSideEvent(worker.chainSideCh)
go worker.update()
go worker.wait()
worker.commitNewWork()
return worker
}
在创建工作实例的时候,会有几个重要的事件,包括TxPreEvent、ChainHeadEvent、ChainSideEvent,我在上面的代码注释中标注了。 我们来看看启动新线程执行的worker.update(),
case <-self.chainHeadCh:
self.commitNewWork()
// Handle ChainSideEvent
case ev := <-self.chainSideCh:
self.uncleMu.Lock()
self.possibleUncles[ev.Block.Hash()] = ev.Block
self.uncleMu.Unlock()
// Handle TxPreEvent
case ev := <-self.txCh:
由于源代码较长,我只展示了一部分。 我们知道update方法是用来监听和处理上面提到的三个事件的。 我们再来看看 worker.wait() 方法,
func (self *worker) wait() {
for {
mustCommitNewWork := true
for result := range self.recv {
atomic.AddInt32(&self.atWork, -1)
if result == nil {
continue
}
block := result.Block
work := result.Work
// Update the block hash in all logs since it is now available and not when the
// receipt/log of individual transactions were created.
for _, r := range work.receipts {
for _, l := range r.Logs {
l.BlockHash = block.Hash()
}
}
for _, log := range work.state.Logs() {
log.BlockHash = block.Hash()
}
stat, err := self.chain.WriteBlockAndState(block, work.receipts, work.state)
if err != nil {
log.Error("Failed writing block to chain", "err", err)
continue
}
// 检查是否是标准块,写入交易数据。
if stat == core.CanonStatTy {
// 受ChainHeadEvent事件的影响。
mustCommitNewWork = false
}
// 广播一个块声明插入链事件NewMinedBlockEvent
self.mux.Post(core.NewMinedBlockEvent{Block: block})
var (
events []interface{}
logs = work.state.Logs()
)
events = append(events, core.ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})
if stat == core.CanonStatTy {
events = append(events, core.ChainHeadEvent{Block: block})
}
self.chain.PostChainEvents(events, logs)
// 将处理中的数据插入到区块中,等待确认
self.unconfirmed.Insert(block.NumberU64(), block.Hash())
if mustCommitNewWork {
self.commitNewWork() // 多次见到,顾名思义,就是提交新的work
}
}
}
}
wait方法比较长,但是必须展示,因为里面包含了写block的重要具体操作。 具体可以参考上面代码中的注释。
使用 New 方法初始化并创建矿工实例。 输入参数包括Backend对象、ChainConfig对象属性集、事件锁、指定的共识算法引擎,返回一个Miner指针。 方法体中组装赋值矿机对象,调用NewCpuAgent方法创建代理实例注册到矿机中,并启动单独的线程执行miner.update()。 我们先看NewCpuAgent方法:
func NewCpuAgent(chain consensus.ChainReader, engine consensus.Engine) *CpuAgent {
miner := &CpuAgent{
chain: chain,
engine: engine,
stop: make(chan struct{}, 1),
workCh: make(chan *Work, 1),
}
return miner
}
通过NewCpuAgent方法,首先组装一个CpuAgent,分配ChainReader、共识引擎、停止结构、工作通道,然后将这个CpuAgent实例分配给矿工,并返回矿工。 然后让我们回到 miner.update() 方法:
// update方法可以保持对下载事件的监听,请了解这是一段短型的update循环。
func (self *Miner) update() {
// 注册下载开始事件,下载结束事件,下载失败事件。
events := self.mux.Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{})
out:
for ev := range events.Chan() {
switch ev.Data.(type) {
case downloader.StartEvent:
atomic.StoreInt32(&self.canStart, 0)
if self.Mining() {// 开始下载对应Miner操作Mining。
self.Stop()
atomic.StoreInt32(&self.shouldStart, 1)
log.Info("Mining aborted due to sync")
}
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) // 执行Miner的start方法。
}
// 处理完以后要取消订阅
events.Unsubscribe()
// 跳出循环,不再监听
break out
}
}
}
然后我们再看看矿机的挖矿方式,
// 如果miner的mining属性大于1即返回ture,说明正在挖矿中。
func (self *Miner) Mining() bool {
return atomic.LoadInt32(&self.mining) > 0
}
我们再看看Miner的start方法。 它是属于 Miner 指针实例的方法。 首字母大写表示可以被外部访问,传入一个地址。
func (self *Miner) Start(coinbase common.Address) {
atomic.StoreInt32(&self.shouldStart, 1)
self.worker.setEtherbase(coinbase)
self.coinbase = coinbase
if atomic.LoadInt32(&self.canStart) == 0 {
log.Info("Network syncing, will start miner afterwards")
return
}
atomic.StoreInt32(&self.mining, 1)
log.Info("Starting mining operation")
self.worker.start()
self.worker.commitNewWork()
}
关键代码是 self.worker.start() 和 self.worker.commitNewWork()。 先说worker.start()方法。
func (self *worker) start() {
self.mu.Lock()
defer self.mu.Unlock()
atomic.StoreInt32(&self.mining, 1)
// spin up agents
for agent := range self.agents {
agent.Start()
}
}
worker.start() 实际上遍历所有启动它的代理。 上面说了,这里是CpuAgent的实现。
func (self *CpuAgent) Start() {
if !atomic.CompareAndSwapInt32(&self.isMining, 0, 1) {
return // agent already started
}
go self.update()
}
启用一个单独的线程来执行 CpuAgent 的 update() 方法。 update 方法与上面的 miner.update 非常相似。
func (self *CpuAgent) update() {
out:
for {
select {
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
}
}
}
out:break out,跳出for循环,for循环不断监听self信号,当检测到self停止时,调用closing操作代码,直接pick出循环监听,函数退出。
通过监听CpuAgent的workCh通道,是否有work信号进入,如果有agent,则开始挖矿,挖矿期间会被锁定,并开启一个单独的线程执行CpuAgent的mine方法。
func (self *CpuAgent) mine(work *Work, stop <-chan struct{}) {
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())
self.returnCh <- &Result{work, result}
} else {
if err != nil {
log.Warn("Block sealing failed", "err", err)
}
self.returnCh <- nil
}
}
执行到这里可以看到调用了CpuAgent共识引擎的区块封装函数Seal来进行具体的挖矿操作。
** 前面说过,以太坊中有两种共识算法,ethash和clique,所以对应的Seal方法也有两种实现。 这是一个技巧,我们将在以后的博文中详细介绍它们。 **
这里先打个坑,回到上面继续分析另一个重要的方法self.worker.commitNewWork()。 commitNewWork方法的源码比较长,这里就不贴了。 该方法的主要工作是为新区块准备基础数据,包括header、txs、uncles等。
叔块的概念
区块链中存在一种可能,由于网络原因,一个区块不存在于最长的链上,这个区块称为孤块。 一般来说,区块链提倡最长就是正义,会毫不犹豫的剔除孤块,但是叔块的挖矿也会消耗大量的能量,合法,但不在最长链上。 在以太坊中,孤立块被称为叔块,不会被视为一文不值,而是会得到奖励。
让我们换一种方式来解释叔块。 当一个区块即将出块时,两个节点可能同时出块。 这时候区块链会保留这两个区块,然后看哪个区块先有后继区块。 谁被领养,另一块被淘汰(谁是儿子,谁是老大,淘汰另一个)。 在以太坊中,生了儿子的老大会称为官方区块,但叔块的矿工也会获得1/32的奖励。 同时,如果老板的儿子记录了叔块,他也会获得额外的奖励,但是打包好的交易本身会回到交易池中,等待再次打包。 这样一来,以太坊就显得非常人性化,相当于对挖叔块工作量的一种认可,是从公平的角度设计的。
总结
以太坊挖矿源码粗略分析到此结束。 粗略的意思是,对于我自己的标准,我没有一一介绍每一个过程控制,以及每一行代码的具体含义,只是提供了一个大概的概况。 看源码路由,一个一个进入,然后收紧返回,最后完成一个闭环,让我们了解一下以太坊挖矿的一些具体操作。 这部分源码的主要工作是交易数据池的维护,区块数据的组织,各种事件的监控和处理,以及miner-worker-agent之间的分工。 最后,剩下的唯一问题就是区块共识,也就是决定谁来生成区块的算法。 我们将在下一篇文章中继续介绍。
参考
go-ethereum源码,网上资料
更多文章请前往醒醒博客园。