主页 > 如何下载imtoken > 以太坊源码机制:挖矿

以太坊源码机制:挖矿

如何下载imtoken 2023-01-18 11:22:44

狗年吉祥,开工有利可图。我们继续研究以太坊的源代码。从这篇文章开始,我们将深入了解以太坊的核心源码,进而分析研究以太坊的核心技术。

关键词:拜占庭、挖矿、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 博客园。