以太坊源码之挖矿与区块确认

大家都知道,以太坊前期的共识算法是工作量证明(POW),可以简单的理解为下述的公式:
RAND(h, nonce) <= M / d
其中h表示区块头的哈希;nonce表示一个自增的变量;RAND表示经过一系列算法生成数值;M表示一个极大的数;d则是当前区块的难度值header.diffculty。
当组装好区块数据后,程序通过不断尝试不同的nonce数值进行RAND运行,使其满足M / d即为挖矿成功,下面我将结合代码对这块进行介绍。
在Miner启动过程分析中,Agent会启动一个通道监听 Work,其源码如下:

func (self *CpuAgent) Start() {
	if !atomic.CompareAndSwapInt32(&self.isMining, 0, 1) {
		return // agent already started
	}
	go self.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
		}
	}
}

在Work将交易数据按照指定的格式封装好之后,调用 self.push(work) 函数向Agent通知新区块,其源码如下:

func (self *worker) push(work *Work) {
	if atomic.LoadInt32(&self.mining) != 1 {
		return
	}
	for agent := range self.agents {
		atomic.AddInt32(&self.atWork, 1)
		if ch := agent.Work(); ch != nil {
			ch <- work
		}
	}
}

从上述两个代码片段可以知道,当Work封装好区块数据后,通过约定的channel 发送区块数据,Agent收到区块数据后启用self.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
	}
}

共识挖矿算法(go-ethereum\consensus\ethash\sealer.go):
func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {
	// ModeFake 或者 ModeFullFake 模式立即返回
	if ethash.config.PowMode == ModeFake || ethash.config.PowMode == ModeFullFake {
		header := block.Header()
		header.Nonce, header.MixDigest = types.BlockNonce{}, common.Hash{}
		return block.WithSeal(header), nil
	}
	
   // 共享模式,转到它的共享对象执行Seal操作
	if ethash.shared != nil {
		return ethash.shared.Seal(chain, block, stop)
	}
	// 创建channel,用于退出和发现nonce时发送事件
	abort := make(chan struct{})
	found := make(chan *types.Block)
	
    // 线程锁
	ethash.lock.Lock()
	// 获取挖矿线程
	threads := ethash.threads
	if ethash.rand == nil {
	    // 获取种子seed
		seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
		if err != nil {
			ethash.lock.Unlock()
			return nil, err
		}
		// 执行成功,拿到合法种子seed,通过其获得rand对象,赋值
		ethash.rand = rand.New(rand.NewSource(seed.Int64()))
	}
	ethash.lock.Unlock()
	if threads == 0 {
	    // 如果设定的线程数为0,则实际线程数同CPU数
		threads = runtime.NumCPU()
	}
	if threads < 0 {
		threads = 0 // Allows disabling local mining without extra logic around local/remote
	}
 
    // 创建一个计数的信号量
	var pend sync.WaitGroup
	for i := 0; i < threads; i++ {
		pend.Add(1)
		go func(id int, nonce uint64) {
			defer pend.Done()
			// 挖矿工作
			ethash.mine(block, id, nonce, abort, found)
		}(i, uint64(ethash.rand.Int63()))
	}
	
	// 一直等到找到符合条件的nonce值
	var result *types.Block
	select {
	case <-stop:
		// 停止信号
		close(abort)
	case result = <-found:
		// 其中有线程找到了合法区块
		close(abort)
	case <-ethash.update:
		 // 重启信号
		close(abort)
		pend.Wait()
		return ethash.Seal(chain, block, stop)
	}
	
	// 等待所有矿工终止并返回该区块
    // Wait判断信号量计数器大于0,就会阻塞
	pend.Wait()
	return result, nil
}

通过上述代码,可以看出ethash.mine()函数才是正真的挖矿部分代码,通过区块头和随机数nonce不同组合尝试计算哈希值,知道满足条件才算挖矿成功,起源码如下:

func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
	//从区块中获取相关数据
	var (
		header  = block.Header()
		hash    = header.HashNoNonce().Bytes()
		target  = new(big.Int).Div(maxUint256, header.Difficulty)
		number  = header.Number.Uint64()
		dataset = ethash.dataset(number)
	)
	
	var (
	    // 初始化一个变量来表示尝试次数
		attempts = int64(0)
		// 初始化nonce值,后面该值会递增
		nonce    = seed
	)
	
	logger := log.New("miner", id)
	logger.Trace("Started ethash search for new nonces", "seed", seed)
	
search:
	for {
		select {
		case <-abort:
			// 停止信号
			logger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)
			ethash.hashrate.Mark(attempts)
			break search

		default:
	
			attempts++
			if (attempts % (1 << 15)) == 0 {
			    //尝试次数达到2^15,更新hashrate 
				ethash.hashrate.Mark(attempts)
				attempts = 0
			}
			// 计算当前nonce的pow值
			digest, result := hashimotoFull(dataset.dataset, hash, nonce)
			if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
				// 找到合法的nonce值,为header赋值
				header = types.CopyHeader(header)
				header.Nonce = types.EncodeNonce(nonce)
				header.MixDigest = common.BytesToHash(digest)

				// Seal and return a block (if still needed)
				select {
				case found <- block.WithSeal(header):
					logger.Trace("Ethash nonce found and reported", "attempts", nonce-seed, "nonce", nonce)
				case <-abort:
					logger.Trace("Ethash nonce found but discarded", "attempts", nonce-seed, "nonce", nonce)
				}
				break search
			}
			//nonce自增不断进行尝试
			nonce++
		}
	}
	
	runtime.KeepAlive(dataset)
}

通过nonce计算目标值:
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
   // 定义一个lookup函数,用于在数据集中查找数据
	lookup := func(index uint32) []uint32 {
		offset := index * hashWords
		return dataset[offset : offset+hashWords]
	}
    // 将原始数据集进行了读取分割,然后传给hashimoto函数
	return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup)
}

func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {
	// 计算数据集理论的行数
	rows := uint32(size / mixBytes)

	// 合并header和nonce到一个40bytes的seed
	seed := make([]byte, 40)
	copy(seed, hash)
	binary.LittleEndian.PutUint64(seed[32:], nonce)
    
    // 将seed进行Keccak512加密
	seed = crypto.Keccak512(seed)
	seedHead := binary.LittleEndian.Uint32(seed)

	// 开始与重复seed的混合 mixBytes/4 = 128/4=32
    // 长度32,元素uint32 mix占4*32=128bytes
	mix := make([]uint32, mixBytes/4)
	for i := 0; i < len(mix); i++ {
		mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:])
	}
	
	// 定义一个temp,与mix结构相同,长度相同
	temp := make([]uint32, len(mix))

	for i := 0; i < loopAccesses; i++ {
		parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows
		for j := uint32(0); j < mixBytes/hashBytes; j++ {
			copy(temp[j*hashWords:], lookup(2*parent+j))
		}
		// 将mix中所有元素都与temp中对应位置的元素进行FNV hash运算
		fnvHash(mix, temp)
	}
	
	// 对Mix进行混淆
	for i := 0; i < len(mix); i += 4 {
		mix[i/4] = fnv(fnv(fnv(mix[i], mix[i+1]), mix[i+2]), mix[i+3])
	}
	// 保留8个字节有效数据
	mix = mix[:len(mix)/4]
    // 将长度为8的mix分散到32位的digest中去
	digest := make([]byte, common.HashLength)
	for i, val := range mix {
		binary.LittleEndian.PutUint32(digest[i*4:], val)
	}
	return digest, crypto.Keccak256(append(seed, digest...))
}

上述就是POW进行挖矿的全部代码!下面我们来介绍一下怎么对区块进行有效验证的。区块的有效性验证一般分以下几个步骤:
1.验证区块头:调用Ethash.VerifyHeaders()
2.验证区块内容:调用BlockValidator.VerifyBody()(内部还会调用Ethash.VerifyUncles())
接下来先介绍一下区块头的验证,其源码如下:

func (ethash *Ethash) verifyHeader(chain consensus.ChainReader, header, parent *types.Header, uncle bool, seal bool) error {
	// 区块头携带的数据小于 32 
	if uint64(len(header.Extra)) > params.MaximumExtraDataSize {
		return fmt.Errorf("extra-data too long: %d > %d", len(header.Extra), params.MaximumExtraDataSize)
	}
	// 区块时间验证
	if uncle {
		if header.Time.Cmp(math.MaxBig256) > 0 {
			return errLargeBlockTime
		}
	} else {
	   //区块时间戳超前当前时间不得大于15s
		if header.Time.Cmp(big.NewInt(time.Now().Add(allowedFutureBlockTime).Unix())) > 0 {
			return consensus.ErrFutureBlock
		}
	}
	//区块时间戳大于父区块
	if header.Time.Cmp(parent.Time) <= 0 {
		return errZeroBlockTime
	}
	
	// 通过父区块计算难度值 与 区块的难度值进行比较
	expected := ethash.CalcDifficulty(chain, header.Time.Uint64(), parent)
	if expected.Cmp(header.Difficulty) != 0 {
		return fmt.Errorf("invalid difficulty: have %v, want %v", header.Difficulty, expected)
	}
	
	//gasLimit不能大于 <= 2^63-1
	cap := uint64(0x7fffffffffffffff)
	if header.GasLimit > cap {
		return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, cap)
	}
	
	// 消耗的gas必须小于gas limit
	if header.GasUsed > header.GasLimit {
		return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit)
	}

	// 当前gas limit和父块gas limit的差值必须在规定范围内
	diff := int64(parent.GasLimit) - int64(header.GasLimit)
	if diff < 0 {
		diff *= -1
	}
	limit := parent.GasLimit / params.GasLimitBoundDivisor
	if uint64(diff) >= limit || header.GasLimit < params.MinGasLimit {
		return fmt.Errorf("invalid gas limit: have %d, want %d += %d", header.GasLimit, parent.GasLimit, limit)
	}
	
	// 当前区块与父区块 高度值相差1
	if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 {
		return consensus.ErrInvalidNumber
	}
	
	// 调用ethash.VerifySeal()检查工作量证明
	if seal {
		if err := ethash.VerifySeal(chain, header); err != nil {
			return err
		}
	}
	
	// 验证硬分叉相关的数据
	if err := misc.VerifyDAOHeaderExtraData(chain.Config(), header); err != nil {
		return err
	}
	if err := misc.VerifyForkHashes(chain.Config(), header, uncle); err != nil {
		return err
	}
	return nil
}

验证区块内容,其源码如下:

func (v *BlockValidator) ValidateBody(block *types.Block) error {
	// 验证当前数据库中是否已经包含了该区块
	if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) {
		return ErrKnownBlock
	}
	
	// 验证当前数据库中是否已经包含了该区块的父区块
	if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) {
		if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) {
			return consensus.ErrUnknownAncestor
		}
		return consensus.ErrPrunedAncestor
	}
	
	// 验证当前数据库中是否包含该区块的父块
	header := block.Header()
	if err := v.engine.VerifyUncles(v.bc, block); err != nil {
		return err
	}
	//验证叔块的有效性及其hash值
	if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash {
		return fmt.Errorf("uncle root hash mismatch: have %x, want %x", hash, header.UncleHash)
	}
	//计算块中交易的hash值 & 验证是否和区块头中的hash值一致
	if hash := types.DeriveSha(block.Transactions()); hash != header.TxHash {
		return fmt.Errorf("transaction root hash mismatch: have %x, want %x", hash, header.TxHash)
	}
	return nil
}

到此,大致将以太坊区块挖矿和确认的过程解析完!后续有时间,我将把一笔交易的发起,执行,最终存在区块链的过程通过关系图的方式展现给大家,方便大家更加直观的了解其过程!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值