大家都知道,以太坊前期的共识算法是工作量证明(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
}
到此,大致将以太坊区块挖矿和确认的过程解析完!后续有时间,我将把一笔交易的发起,执行,最终存在区块链的过程通过关系图的方式展现给大家,方便大家更加直观的了解其过程!