【源码阅读】blockchainⅡ

1

1.1 NewBlockHandler

func (bc *BlockChain) NewBlockHandler(payload []byte, peer peer.ID) error {

该函数是对新的区块的数据处理,payload是一个字节切片,表示新块的数据;peer是一个peer.ID类型的变量,表示发送新块数据的对等节点。:

  1. 使用err := proto.Unmarshal(payload, &nweBlock)payload反序列化为nweBlock对象,如果有错误err,就输出错误日志并返回err
  2. 如果反序列化成功,函数会继续执行。
  3. 使用block.FromProtoMessage方法将nweBlock中的新块数据转换为block对象。
  4. 如果转换过程中没有错误,次使用block.FromProtoMessage方法将nweBlock中的新块数据转换为block对象
  5. bc.chBlocks <- &blockblock对象发送到bc.chBlocks通道中。
    如果在整个过程中没有出现任何错误,函数最终会返回nil

1.2 SetEngine

func (bc *BlockChain) SetEngine(engine consensus.Engine) {
	bc.engine = engine
}

用于设置区块链的engine入口

1.3 SealedBlock

func (bc *BlockChain) SealedBlock(b block2.IBlock) error {
	pbBlock := b.ToProtoMessage()
	return bc.p2p.Broadcast(context.TODO(), pbBlock)
}

将块信息转换为message广播出去,通过p2p协议的bc.p2p.Broadcast(context.TODO(), pbBlock)进行

1.4 StopInsert

func (bc *BlockChain) StopInsert() {
	atomic.StoreInt32(&bc.procInterrupt, 1)
}

atomic.StoreInt32(&bc.procInterrupt, 1)是Go语言中的一个原子操作,用于将整数1存储到变量bc.procInterrupt的内存地址中。用于中断插入进程。

1.5 insertStopped

func (bc *BlockChain) insertStopped() bool {
	return atomic.LoadInt32(&bc.procInterrupt) == 1
}
  1. atomic.LoadInt32(&bc.procInterrupt)是Go语言中的一个原子操作,用于从变量bc.procInterrupt的内存地址中加载整数。
  2. insertStopped 返回 true 表示在调用 StopInsert 之后,插入insertStopped 返回 true 表示在调用 StopInsert 之后,插入操作已经停止。

1.6 InsertChain(*)

func (bc *BlockChain) InsertChain(chain []block2.IBlock) (int, error) {

该函数用于插入区块,是一个很重要的函数

  1. 首先判断插入的区块长度是否为0,为0就是不合法的区块,就直接返回nil
  2. 遍历整个chain区块,进行for循环
  • 创建变量block和prev分别存储当前一个区块和前一个区块
  • 如果区块的编号不是前一个区块编号加1,或者区块的父哈希与前一个区块的哈希不相等,就会输出错误信息
if block.Number64().Cmp(uint256.NewInt(0).Add(prev.Number64(), uint256.NewInt(1))) != 0 || block.ParentHash() != prev.Hash() {
  1. 初始循环没有错之后,就会上锁,进行插入bc.lock.Lock()
  2. 并使用延迟释放锁defer bc.lock.Unlock(),确保在函数完成之后会释放锁
  3. 调用bc.insertChain(chain)进行插入(参考1.7)

1.7 insertChain(*)

func (bc *BlockChain) insertChain(chain []block2.IBlock) (int, error) {

这也是一个很重要的函数

  1. 先判断是否有插入的中断标志bc.insertStopped(),如果有,就直接返回0和nil
  2. 创建一些变量:开始时间的计时,头部headers和密封验证标志seals(可以用来在验证的时候选择是否要进行密封验证)等
  3. 遍历整个要插入的chain,对headers和seals进行赋值(block.Header()和true
  4. 进行验证:调用bc.engine.VerifyHeaders对区块的头部和seals进行验证,并返回abort和reaults
    其中使用到的函数来自internal/consensus/cpnsensus.go中engine的函数,VerifyHeaders类似于VerifyHeader,但同时验证一批标头。该方法返回一个退出通道以中止操作,并返回一个结果通道以检索异步验证(顺序为输入切片的顺序)。
  5. 使用延迟函数defer close(abort)在函数完成的时候关闭通道
  6. 创建一个迭代器,调用的是blockchain_insert.go里面的函数
  7. block, err := it.next()//next返回迭代器中的下一个块,以及该块的任何潜在验证错误。当到达终点时,它将返回(nil,nil)。
  8. 调用bc.skipBlock(err)(参考2.4),如果错误是已知的,就会执行该if语句
  • 两个变量:是否reorg的标志和当前区块
  • 如果区块不为nil且err已知,就会执行for循环:
    (1)调用bc.forker.ReorgNeeded(参考1.15)返回是否应基于给定的外部标头和本地规范链应用reorg
    (2)如果有错误,就返回当前执行位置index和错误err
    (3)否则按照reorg的值来决定是否执行if语句(这是在分叉部分决定)
    如果forker说reorg是必要的,并且块不在规范链上,则切换到导入模式。
    在eth2中,forker总是为reorg决策返回true(盲目信任外部共识引擎),但为了防止在导入已知块时发生不必要的reorg,这里处理特殊情况:
    如果需要并且新区块不在规范链上,则切换到导入模式。在导入已知区块时,为了防止不必要的重组织,这里会特殊处理这种情况。
    (4)打印日志信息
    (5)如果区块已经被忽略,它会记录忽略的区块数量,并获取下一个区块。
  1. 在block不为Nil和错误已知的时候执行
for block != nil && bc.skipBlock(err) {
	log.Debug("Writing previously known block", "number", block.Number64(), "hash",block.Hash())
	if err := bc.writeKnownBlock(nil, block); err != nil {
		return it.index, err
	}
	lastCanon = block
	block, err = it.next()
}

writeKnownBlock使用已知块更新头块标志,并在必要时引入chain reorg
10. 进行switch语句选择,根据不同的错误类型进行不同的操作

  • 如果错误是ErrPrunedAncestor(表示第一个区块被修剪)errors.Is(err, ErrPrunedAncestor),则将其插入为侧链bc.insertSideChain(block, it),并在TD增长到足够大时进行重组织。
  • 如果错误是ErrFutureBlock(表示第一个区块是未来的)errors.Is(err, ErrFutureBlock)或者错误是ErrUnknownAncestor(errors.Is(err, ErrUnknownAncestor)且未来区块包含当前区块的父哈希bc.futureBlocks.Contains(it.first().ParentHash()),则将该区块及其所有子区块推迟到未来队列中(未知祖先)err := bc.AddFutureBlock(block),同时根据迭代器修改stats中的忽视数量和queued数量
  • 如果出现除ErrKnownBlock之外的其他错误,会从未来区块列表中移除该区块的哈希值bc.futureBlocks.Remove(block.Hash()),增加忽略的区块数量,并报告该区块的错误信息bc.reportBlock(block, nil, err)。最后返回当前索引和错误信息。
  1. 955-984用于创建evmRecord
    接受以下参数:
  • ctx:一个上下文对象,用于传递额外的信息给函数。
  • db:一个读写数据库对象,用于执行数据库操作。
  • blockNr:一个无符号64位整数,表示区块编号。
  • f:一个函数,接受四个参数:事务对象tx、内部区块状态对象ibs、状态读取器对象reader和状态写入器对象writer。该函数返回两个值:一个映射表和一个错误对象。
    函数的主要逻辑如下:
  • 开始一个只读事务db.BeginRo(ctx),如果发生错误则返回空指针和错误对象
  • 使用延迟回滚
  • 创建一个状态读取器对象,使用事务对象作为参数state.NewPlainStateReader(tx)
  • 创建一个内部区块状态对象,使用状态读取器对象作为参数state.New(stateReader)
  • 创建一个状态写入器对象,这里使用了state.NewNoopWriter()
  • 调用传入的函数f,并将事务对象、内部区块状态对象、状态读取器对象和状态写入器对象作为参数传递给它nopay, err = f(tx, ibs, stateReader, stateWriter)。将返回的结果赋值给变量nopayerr
  • 如果发生错误,则返回空指针nil和错误对象err
  • 提交事务并返回内部区块状态对象、nopay映射表和nil错误对象。

当区块不为nil并且没有错误或者错误为ErrKnownBlock的时候,不断遍历blocks:

  1. 开始计时插入时间start := time.Now(),创建变量收据、日志和gas
  2. 通过调用函数evmRecord,其中传入的参数f如下:
  • 通过rawdb.ReadHeader(tx, hash, number)获取到对应的区块头
  • 通过GetHashFn(block.Header().(*block2.Header), getHeader)返回对应的hash(参考Ⅰ)
  • 处理计时开始
  • 调用bc.process.Process方法来处理区块,并返回一些结果,包括收据、未支付的交易、日志、使用的gas以及可能的错误。如果处理过程中出现错误,它会报告这个错误并返回nil和err
  • 处理执行计时完成
  • 验证计时开始vstart := time.Now()
  • 调用bc.validator.ValidateState方法来验证区块的状态。如果验证失败,它会报告这个错误bc.reportBlock(block, receipts, err)并返回
  • 验证计时结束
  • 得到执行和验证的时间
  1. 如果调用evmRecord的过程中存在err,就返回当前的index和err
  2. 写状态计时开始wstart := time.Now()
  3. 调用status, err = bc.writeBlockWithState(block, receipts, ibs, nopay)来写块,有错就返回
  4. 得到写状态时间(15)和插入时间(12)
  5. 更新queued的数量和uesdgas
  6. 根据不同的状态执行不同的操作(switch):
  • 当状态为CanonStatTy规范链时,会记录插入新块的相关信息,并发送日志事件。如果存在日志len(logs) > 0,则发送全局日志事件event.GlobalEvent.Send。最后将最后一个规范块设置为当前块lastCanon = block
  • 当状态为SideStatTy侧链时,会记录插入分叉块的相关信息。
  • 如果状态既不是CanonStatTy也不是SideStatTy,则会记录插入具有未知状态的块的相关信息,并发出警告日志。
    到这里为一整个for循环
  1. 当block为nil并且错误是ErrFutureBlock的时候,我们考虑未来区块
  • 将第一个区块加入到未来区块中bc.AddFutureBlock(block),有错的时候就返回
  • 获取下一个区块,当区块不为nil并且错误为ErrUnknownAncestor的时候,不断遍历获取下一区块
  • 将当前区块加入未来区块中bc.AddFutureBlock(block),有错就返回
  • 更新queued
  1. 更新ignored的数量

1.8 insertSideChain

func (bc *BlockChain) insertSideChain(block block2.IBlock, it *insertIterator) (int, error) {

与1.7不同,该函数用于插入侧链

  1. 创建一些变量用于存储如当前区块和上一个区块等
  2. 当block不为nil且错误为ErrPrunedAncestor的时候,不断调用block.next()来获取下一块进行for循环
  • 检查该数字的规范状态根
  • 获取区块block的区块号number和当前区块号进行对比,当当前区块号大于等于number的时候,会执行:
    (1)通过number获取区块canonicalbc.GetBlockByNumber(number),有错就返回0和nil
    (2)如果canonical不为nil且其hash与主链上block.hash一样,就说明这不是一个侧链区块,该区块是一个重新导入的主链区块,其状态已经被修剪掉了。这个时候直接获取就可以然后continue处理下一个区块
pt := bc.GetTd(block.Hash(), block.Number64())
externTd = *pt

(3)如果给定区块不是主链上的区块,但与主链上的一个区块具有相同的状态根值canonical.StateRoot() == block.StateRoot(),那么它很可能是一个shadow-state攻击。当一个分叉被导入到数据库中,并最终达到一个没有被修剪的高度时,我们会发现状态已经存在。这意味着侧链区块引用了一个已经存在于我们的主链中的状态。如果不及时进行检查,我们将在不验证之前区块状态的情况下导入区块。在这种情况下,代码会返回一个错误信息,表示检测到了侧链攻击。

  • 当当前区块号小于number的时候,会执行:
    (1)externTd.Cmp(uint256.NewInt(0)) == 0:判断 externTd 是否等于0。如果等于0,说明当前区块是创世区块,没有前一个区块,因此需要特殊处理。通过bc.GetTdblock.ParentHash()获取父区块的TD值并将将父区块的TD值赋给 externTd。
    (2)externTd = *externTd.Add(&externTd, block.Difficulty()):将当前区块的难度值与父区块的TD值相加,得到当前区块的总难度值,并更新 externTd
  • 如果区块不在链上的话,就要将区块写上去
    (1)记录当前时间作为开始时间
    (2)调用bc.WriteBlockWithoutState(block)方法将区块写入区块链,如果写入过程中出现错误,返回当前索引和错误信息
    (2)如果写入成功,记录日志信息
  • 更新上一区块,并开始下一轮循环
  1. 根据bc.forker.ReorgNeeded(参考1.15)判断是否需要reorg,如有错就返回当前索引值index和err
  2. 如果不需要reorg,就通过bc.GetTd获得难度td并赋值给localtd,输出日志信息并返回当前索引index和err
  3. 如果需要reorg,创建变量hashes和numbers用于存放hash值和区块号
  4. parent := it.previous()获得前一区块为父区块
  5. 如果父区块不为nil并且检验父区块的状态根不在数据库中!bc.HasState(parent.StateRoot()),执行for循环:
  • 将父区块的hash和number加入hashes和numbers中
  • 通过bc.GetHeader向前遍历父区块
  • 直到父区块不存在或者父区块的状态根已经存在,此时就找到了分叉点
  1. 通过上一步得到的hashes,结合bc.GetBlock,遍历地加入创建地遍历blocks中:
  • 当内存量过大的时候if len(blocks) >= 2048,我们仍然继续导入,但是我们需要输出一个提示日志
  • 如果链正在终止,请停止处理块,并输出log

1.9 recoverAncestors

func (bc *BlockChain) recoverAncestors(block block2.IBlock) (types.Hash, error) {

这段代码是一个名为recoverAncestors的函数,它属于一个名为BlockChain的结构体。该函数的作用是恢复给定区块的所有祖先区块,并将它们插入到区块链中。函数返回两个值:一个是类型为types.Hash的哈希值,表示最后一个成功插入的祖先区块的哈希值;另一个是错误信息,如果发生错误则返回非空的错误对象。

  1. 创建变量存储hash、number和父区块(初始为block)
  2. 不断向前遍历区块bc.GetBlock,将父区块的哈希、区块号依次加入,直到找到具有有效状态的父区块bc.HasState(parent.Hash())或到达区块链的起始点parent == nil
  3. 如果找不到有效的父区块,函数将返回一个空的哈希值和一个错误信息
  4. 函数使用另一个循环来反向遍历祖先区块列表:
  • 根据当前哈希值和编号,获取对应的区块对象bc.GetBlock,并将其插入到区块链中bc.insertChain(参考1.7)。如果插入过程中发生错误,函数将返回当前区块的父哈希值和错误信息
  • 第一次循环的时候特殊处理,此时要处理的区块为传入的区块block
  1. 返回最终祖先区块的哈希值和nil作为错误信息,表示所有祖先区块已成功插入到区块链中

1.10 WriteBlockWithoutState

func (bc *BlockChain) WriteBlockWithoutState(block block2.IBlock) (err error) {

WriteBlockWithoutState仅将块及其元数据写入数据库,但不写入任何状态。 这用于构建竞争方叉,直到超过规范总难度。

  1. 如果中止插入bc.insertStopped(),就返回errInsertionInterrupted错误
  2. 更新数据库数据bc.ChainDB.Update,其中写数据库调用rawdb包中的rawdb.WriteBlock进行,如果有错就返回err

1.11 WriteBlockWithState

func (bc *BlockChain) WriteBlockWithState(block block2.IBlock, receipts []*block2.Receipt, ibs *state.IntraBlockState, nopay map[types.Address]*uint256.Int) error {

该函数WriteBlockWithState将块和所有关联状态写入数据库

  1. 上锁和延迟解锁
  2. 调用bc.writeBlockWithState(参考1.12)来写入数据库

1.12 writeBlockWithState

func (bc *BlockChain) writeBlockWithState(block block2.IBlock, receipts []*block2.Receipt, ibs *state.IntraBlockState, nopay map[types.Address]*uint256.Int) (status WriteStatus, err error) {

该函数WriteBlockWithState将块和所有关联状态写入数据库
函数的参数包括:

  • block:要写入的区块对象,类型为block2.IBlock
  • receipts:与该区块关联的交易收据列表,类型为[]*block2.Receipt
  • ibs:内部区块状态对象,类型为*state.IntraBlockState
  • nopay:一个映射表,表示不需要支付奖励的地址及其对应的奖励值,类型为map[types.Address]*uint256.Int

函数的返回值包括:

  • status:写入状态,类型为WriteStatus。可能的值有NonStatTy(非正常状态)、CanonStatTy(正常状态)和SideStatTy(侧链状态)。
  • err:错误信息,类型为error。如果发生错误,将返回相应的错误信息。

函数的主要逻辑如下:

  1. 使用bc.ChainDB.Update方法更新区块链数据库,执行以下操作:
    • 读取父区块的TDrawdb.ReadTd,ptd,如果为nil就返回consensus.ErrUnknownAncestor
    • 计算externTd,并将其写入数据库rawdb.WriteTd
    • 如果存在交易收据if len(receipts) > 0,将其追加到数据库中rawdb.AppendReceipts
    • 将区块数据写入数据库rawdb.WriteBlock
    • 使用状态机state.NewPlainStateWriter提交区块ibs.CommitBlock,并处理一些额外的操作,如写入更改集stateWriter.WriteChangeSets()和历史记录stateWriter.WriteHistory()
    • 遍历nopay映射表,使用rawdb.PutAccountReward写入数据库。
  2. 检查是否需要进行重组织(reorg)bc.forker.ReorgNeeded(参考1.15),即当前区块是否是头区块的直接子区块。如果是,则进行重组织。
  3. 如果父级不是头块,则重新组织链
  4. 在reorg过程中出错的话就返回NonStatTy,没出错status就为CanonStatTy。如果不需要reorg,status就为SideStatTy
  5. 根据重组织的结果,设置新的头区块bc.writeHeadBlock(参考1.13),并保存最新的区块,在这个过程中出错的话就返回NonStatTy
  6. 如果未来区块中已经存在该区块的哈希值bc.futureBlocks.Get(block.Hash()),则从未来区块列表中移除它
  7. 函数返回写入状态和错误信息

1.13 writeHeadBlock

func (bc *BlockChain) writeHeadBlock(tx kv.RwTx, block block2.IBlock) error {

这段代码是一个名为writeHeadBlock的函数,它的作用是将一个区块(block)写入区块链数据库。函数接收两个参数:一个是键值对读写事务(tx),另一个是要写入的区块(block)。

  1. 检查传入的事务是否为空,如果为空,则创建一个新的读写事务bc.ChainDB.BeginRw(bc.ctx)并标记为非外部事务notExternalTx = true,同时使用延迟回滚
  2. 将区块的哈希值写入数据库两次rawdb.WriteHeadBlockHash
  3. 分别写入头部区块哈希和交易查找条目,WriteTxLookupEntries存储块中每个事务的位置元数据,从而实现基于哈希的事务和收据查找rawdb.WriteTxLookupEntries
  4. 将区块的哈希值和区块号写入数据库中的规范哈希表rawdb.WriteCanonicalHash
  5. 将当前区块设置为传入的区块,并更新头部区块的计数器
  6. 如果事务不是非外部事务if notExternalTx,则提交事务tx.Commit()。如果在执行过程中出现错误,函数会返回相应的错误信息。

1.14 reportBlock

func (bc *BlockChain) reportBlock(block block2.IBlock, receipts []*block2.Receipt, err error) 

该函数用于bad区块的错误日志,遍历传入的收据列表,输出错误信息

1.15 ReorgNeeded

func (bc *BlockChain) ReorgNeeded(current block2.IBlock, header block2.IBlock) bool {
	switch current.Number64().Cmp(header.Number64()) {
	case 1:
		return false
	case 0:
		return current.Difficulty().Cmp(uint256.NewInt(2)) != 0
	}
	return true
}

该函数用于判断是否需要进行reorg重组织
使用switch语句来根据current和header的区块号进行不同的比较操作。

  1. 如果current的区块号大于header的区块号,则返回false,表示不需要重组
  2. 如果两者的区块号相等,则比较current的难度值是否等于2,如果不等于2,则返回true,表示需要重组;否则返回false
  3. 如果current的区块号小于header的区块号,则返回true,表示需要重组

1.16 SetHead

func (bc *BlockChain) SetHead(head uint64) error {
	newHeadBlock, err := bc.GetBlockByNumber(uint256.NewInt(head))
	if err != nil {
		return nil
	}
	return bc.ChainDB.Update(bc.ctx, func(tx kv.RwTx) error {
		return rawdb.WriteHeadHeaderHash(tx, newHeadBlock.Hash())
	})
}

该方法的作用是将区块链的头部设置为指定的区块号。

  1. 根据区块号获得区块bc.GetBlockByNumber,有错就返回nil
  2. 使用bc.ChainDB.Update函数来更新区块链数据库。在更新过程中,它调用了rawdb包中的rawdb.WriteHeadHeaderHash函数,将新的头部区块哈希值写入数据库
  3. 返回更新操作的结果,如果出现错误,则返回该错误。

1.17 AddFutureBlock

func (bc *BlockChain) AddFutureBlock(block block2.IBlock) error {

AddFutureBlock检查该块是否在允许接受的最大窗口内以供将来处理,如果该块太超前且未添加,则返回错误。将一个区块添加到未来区块队列中。

  1. 计算最大时间戳max,它是当前时间加上允许的最大未来区块时间maxTimeFutureBlocks
  2. 检查传入的区块的时间戳是否大于最大时间戳block.Time() > max,如果是,则返回一个错误信息,表示未来区块的时间戳超过了允许的范围。
  3. 检查传入的区块的难度是否为0block.Difficulty().Uint64() == 0,如果是,则不将其添加到未来区块队列中,并返回nil
  4. 如果时间没有超过最大时间且难度不为0,方法会记录一条日志信息,包括区块的哈希值、区块号、状态根和交易数量。然后,将该区块添加到未来区块队列中bc.futureBlocks.Add,并返回nil

2、has判断

2.1 HasBlockAndState

func (bc *BlockChain) HasBlockAndState(hash types.Hash, number uint64) bool {

该函数通过传入的hash和number来判断是否存在对应的区块

  1. 通过block := bc.GetBlock(hash, number)来获取对应的区块block(参考Ⅰ)
  2. 如果获取的block为nil,就返回false,表示不存在这样的区块
  3. 否则就继续返回调用bc.HasState(block.Hash())(参考2.2)判断是否有对应的state

2.2 HasState

func (bc *BlockChain) HasState(hash types.Hash) bool {
  1. 通过bc.ChainDB.BeginRo(bc.ctx)开启一个事务
  2. 开启过程中有err,就返回false
  3. 否则使用rawdb.IsCanonicalHash(tx, hash)函数来判断有没有,有错err就返回false
  4. 否则就返回在数据库中判断的结果is
  5. 使用延迟回滚defer tx.Rollback()

2.3 HasBlock

func (bc *BlockChain) HasBlock(hash types.Hash, number uint64) bool {

该函数通过对应的hash和number来判断是否有对应的区块block

  1. 创建变量flag来表示最后的结果
  2. 先在blockcache缓存中进行判断bc.blockCache.Contains(hash),如果存在就返回true
  3. 否则就开始在数据库中查找判断
  4. 开启bc.ChainDB.View,其中调用数据库包rawdb中的rawdb.HasHeader(tx, hash, number)来进行判断,并将结果赋值给flag
  5. 最后返回flag

2.4 skipBlock

func (bc *BlockChain) skipBlock(err error) bool {
	if !errors.Is(err, ErrKnownBlock) {
		return false
	}
	return true
}

这段代码是一个名为 skipBlock 的函数,它接受一个错误参数 err。该函数的作用是检查传入的错误是否为已知的块错误(ErrKnownBlock)。如果是已知的块错误,则返回 true,否则返回 false。

你可能感兴趣的:(区块链,区块链)