大家都知晓,在区块链领域里,“挖矿”是一件引人瞩目的事情。在以太坊的挖矿过程中,矿工以及 POW 算法发挥着关键的作用,并且它的源码也值得去进行探究。接下来就为大家详细地进行讲述。

矿工与挖矿的含义

矿工的工作是“挖矿”,在区块链的世界中这极为重要。挖矿意味着将一系列尚未封装到块中的交易,封装进一个新的区块里。像以太坊这类区块链项目,矿工通过持续的计算,把交易数据打包进区块,使得整个区块链网络能够正常运转,确保数据能有序地被记录和更新。

不同的区块链对矿工的要求各异。不过,其中多数都需完成计算任务。例如在以太坊里,矿工要挖出新区块,就必须进行大量的计算以及验证工作。只有通过这样的方式,他们才能获得对应的奖励,从而激励他们持续为整个网络的稳定运行作出贡献。

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 // 同步以后是否应该开始挖矿
}

POW算法的作用

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
}

POW 算法是解决区块链正确性的重要方案。它借助哈希函数达成目的,提供了一种计算难度大但验证容易的方式。哈希函数具有独特特性,这使得 POW 算法能发挥巨大作用。在以太坊网络中,POW 算法如同一个守护者,保障着区块链数据的安全与一致性。

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"`
}

矿工在进行挖矿操作时,POW 算法会促使各结点依据特定规则展开计算。每个结点都需要持续不断地进行尝试,直至找到满足条件的加密哈希。这样的一个过程尽管会耗费时间以及资源,然而却能够保障整个网络的稳定与安全,它是区块链技术当中不可或缺的一个重要部分。

// Agent 可以注册到worker
type Agent interface {
    Work() chan<- *Work
    SetReturnCh(chan<- *Result)
    Stop()
    Start()
    GetHashRate() int64
}

挖矿的具体过程

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是否正在挖矿的标志位
}

各结点在接收到 POW 算法所发布的哈希加密函数之后,便开始了挖矿的进程。它们将待封印的区块信息与一个 nonce 值进行结合,借助该函数来计算加密哈希。此加密哈希需要满足特定的规则,例如其前四位必须是 1111 。结点仅能通过穷举法持续地进行尝试。只有当找到满足条件的哈希时,才意味着出块获得了成功。

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
}

当出块成功之时,结点会将区块哈希进行广播。其他结点在收到此广播后,会展开验证工作。倘若发现确实符合预定的规则,便会达成共识,而这个块也就由刚刚进行广播的结点完成出块。整个这一过程需要各个结点相互配合并且进行验证,以此保证了区块链的可信度。

共识规则的变化

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
}

在区块链网络里,有时某个结点会把新的共识规则给发布出来。通常情况下,新规则具备向前兼容的特性,这意味着链上之前的那些数据依然是有效的。然而,那些没有同步新规则的结点,依旧会依照原来的规则去进行挖矿。

未同步新规则的结点所挖出的块,不会被更新了新规则的结点所共识,也不会被其承认。这样就有可能引发区块链网络的分叉,不同的结点会依据不同的规则运行,从而对整个网络的稳定性和一致性造成影响。所以,新规则的发布以及结点的同步必须谨慎对待。

        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:

以太坊源码中的矿工实例创建

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
            }
        }
    }
}

在对以太坊 miner.go 文件的源码进行分析时,创建 miner 实例是很重要的。在创建过程中,会依据 Miner 结构体的成员属性依次进行赋值,其中有一些对象是需要调用构造函数的。这就如同在搭建一座房子,每一个组件都有着它的作用和所处的位置。

通过给成员属性赋值,使得 miner 实例可以正常运行。它犹如机器的核心部分,掌控着整个挖矿过程的运行与管理,保证以太坊的挖矿工作能够有序开展。

源码中的重要事件与操作

在创建 work 实例的时候,会出现几个重要的事件。这些事件在代码的注释里都有明确的标识,对于了解以太坊的挖矿过程是很有帮助的。例如 wait 方法,它虽然比较长,但是其中包含了重要的、具体的写入块的操作。

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
}

在方法体里,会先对 miner 对象进行组装并赋值。接着调用方法来创建 agent 的一个实例,之后将这个实例注册到 miner 上。随后启动一个单独的线程去执行 miner 的相关方法。通过对通道进行监测,一旦有 work 工作信号进入,agent 就会开始挖矿。这里的每一个操作都相互紧密关联,一起共同完成以太坊的挖矿任务。

// 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
}