主页 > 如何下载imtoken > 以太坊源码机制:挖矿
以太坊源码机制:挖矿
狗年吉祥,开工有利可图。我们继续研究以太坊的源代码。从这篇文章开始,我们将深入了解以太坊的核心源码,进而分析研究以太坊的核心技术。
关键词:拜占庭、挖矿、miner、fork、源码分析、大叔大叔、agent、worker、事件监控
本文基于go-ethereum 1.7.3-stable源码版本。源码范围主要在矿工pkg中。
miner.start()
矿工是指矿工。矿工要做的工作就是“挖矿”。挖矿是将一系列尚未封装成区块的最新交易封装成新区块的过程。在学习以太坊挖矿之前,我们首先要了解几个概念:
拜占庭将军问题
分布式系统中的状态同步问题。
拜占庭帝国繁荣昌盛,周边几个小国的将领觊觎已久,但各有各的想法。他们必须有半数以上同意进攻拜占庭的将领,并且不能在战场上做出背叛行为(共识),否则进攻会失败并被烧毁。将军的领地可以分给其他几位将军。基于这种情况,将领之间的沟通非常有问题,有的人彼此不和,有的人忠于组织的利益。最终如何达成共识是个问题。
分布式系统中的每个节点都是一个将军,当这些节点想要同步自己的状态时,就会面临拜占庭将军的问题。如何有效避免节点发送的错误信息对结果的影响?
POW(工作证明)
工作量证明,顾名思义,就是证明你做了多少工作。 POW是目前最流行的解决上述拜占庭将军问题的共识算法,比特币、以太坊等主流区块链数字货币都是基于POW的。
为了解决拜占庭将军问题,我们首先需要确定一个方法:在这些平等的将军中选出一个忠诚的“将军”,其他将军可以听他的决定。
这似乎违背了去中心化的思想,但仔细分析可以看出,这些将军在做出这个决定之前都是去中心化的平等节点,而被选中的将军只是为了这个决定。 ,下一个决定将是重新选择。未来几十年,他的命令将在朝廷内外服从,而不是集中任命一位将军。
弄清楚了这个问题之后,接下来要解决的就是如何选择武将了。在决策之前,这些将军是平等的节点,所以需要通过他们在决策时的讲话来判断。将领们通过已知的战场信息,估算出当前的战场形势,进行计算,并将结论(块)广播给其他将领。同时,将军必须时刻监视其他将军的广播内容。一旦收到其他将领的结论广播,将立即停止手中的计算,以验证广播的内容。如果所有将军都通过了验证,那么第一个发送这个可验证结果广播的将军就被选为将军,这次我决定听听他的结论(广播中已经包含的结果内容)。
在这个过程中有两个重要的因素:
首先是速度。第一个通过验证的人可以选为将军。第二个较慢的人没有机会。然后是正确性,即将出兵的结论是否正确,是否能被其他将领成功验证。
速度的问题就是算力的问题。比如我的电脑是8核32G配置,计算能力肯定比你单核1G内存快。这与 POW 算法关系不大。 POW算法用于求解正确性解。 POW 提供了一种难以计算且易于验证的方法。这是基于散列函数的特性。上一篇文章中也介绍过。哈希函数是
通过这些特性,工作量证明可以给每个节点下发一个哈希加密函数,每个节点通过这个函数在要封存的区块信息上加上一个nonce值来计算一个加密的哈希值。希望满足一定的规则(比如前四位必须是1111),每个节点只能通过穷举法继续尝试,直到满足条件,即得出结论到达,即出块成功,当再次广播出块hash时,其他节点验证确实符合预定规则(前四位确实是1111),则共识完成,而区块是由刚刚广播的节点产生的,这个工作量是指节点不断尝试计算的工作量,得到满足条件的区块哈希后,广播后,其他节点正在进行和完成的工作量为无效(其实这也是算力的浪费。),证明区块是你的。
问题 1:过滤块
也就是说,当你广播时,某个其他节点也计算出满足条件的哈希值,即该节点也产生一个相同编号的块。这时候就需要比较两个区块的时间戳了。 ,较早的将被确认并保留在链上,而较晚的将被丢弃。
问题2:分叉链
当一个节点发布新的共识规则时,其他节点不会同步该共识规则。一般来说,新的共识规则是前向兼容的,即链上之前的数据仍然有效且被认可。 ,但是没有同步新规则的节点会继续挖矿,他们挖出的区块不会被更新新规则的节点共识或认可。此时链分叉,分为两条链: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 member对象,可以理解为一个worker,负责所有具体的挖矿工作流。
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
}
复制
worker的属性非常多且具体,都与挖矿的具体操作有关,包括链本身的属性和区块数据结构的属性。先看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属性后面的`内容有疑问,也就是结构体中的标签。它是可选的,是变量的附加内容,可以通过反射包读取。通过观察ChainConfig结构中的attribute标签可以看出,这些标签用于在结构转换中声明变量,是json结构后的id值,这个值可以和当前的变量名不同。
言归正传,ChainConfig包含ChainID等属性,其中有很多是专门针对以太坊历史上出现的问题而配置的。
代理
一个矿工有一个工人以太坊免费挖矿,一个工人有多个代理。 Agent接口在Worker.go文件中定义:
// Agent 可以注册到worker
type Agent interface {
Work() chan<- *Work
SetReturnCh(chan<- *Result)
Stop()
Start()
GetHashRate() int64
}
复制
这个接口有两个实现:CpuAgent 和 RemoteAgent。此处使用 CpuAgent。 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
}
复制
创建矿工实例时,会根据矿工结构的成员属性进行赋值,其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方法比较长,但是必须显示的原因是它包含了重要的具体写块操作。详情见上述代码中的注释。
通过 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并返回miner。那我们回到 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() 方法。更新方法和上面的 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 stop时,调用shutdown操作代码,直接挑出loop监听,函数退出。
通过监控CpuAgent的workCh通道,是否有工作信号进入,如果有代理则开始挖矿。期间一定要加锁,会激活一个单独的线程来执行CpuAgent的挖矿方法。
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的奖励。同时,如果老板的儿子记录了叔块,他也会获得额外的奖励,但叔块也会得到奖励。自己打包的交易会返回到交易池,等待再次打包。这样一来,以太坊就显得很人性化了,相当于对挖叔块工作量的一种认可,是从公平的角度来设计的。
总结
这是对以太坊挖矿源码的粗略分析。粗略的意思是以太坊免费挖矿,对于我自己的标准,我并没有一一介绍每个过程控制,以及每一行代码的具体含义,只是大致提供了一个看源码的路线,一一输入,然后收紧后退,最后完成一个闭环,让我们了解以太坊挖矿的一些具体操作。这部分源码的主要工作是交易数据池的维护、区块数据的组织、各种事件的监控和处理以及矿工-工人-代理之间的分工。最后,就剩下唯一的出块共识问题了,也就是决定谁出块的算法,下一篇我们会继续介绍。
参考文献
go-ethereum源码,网上资料
更多文章,请前往 Awake 博客园。