主页 > imtoken苹果手机怎么下载 > 以太坊源码机制:挖矿【通俗易懂】

以太坊源码机制:挖矿【通俗易懂】

imtoken苹果手机怎么下载 2023-04-26 06:11:38

狗年吉祥,继续研究以太坊源码。 从本文开始,我们将深入以太坊的核心源码,进而对以太坊的核心技术进行分析和研究。 关键词:拜占庭,挖矿,矿工,分叉,源代码分析,叔块,代理,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源码,网上资料

更多文章请前往醒醒博客园。