1
1.1 NewBlockHandler
func (bc *BlockChain) NewBlockHandler(payload []byte, peer peer.ID) error {
该函数是对新的区块的数据处理,payload
是一个字节切片,表示新块的数据;peer
是一个peer.ID
类型的变量,表示发送新块数据的对等节点。:
- 使用
err := proto.Unmarshal(payload, &nweBlock)
将payload
反序列化为nweBlock
对象,如果有错误err,就输出错误日志并返回err - 如果反序列化成功,函数会继续执行。
- 使用
block.FromProtoMessage
方法将nweBlock
中的新块数据转换为block
对象。 - 如果转换过程中没有错误,次使用
block.FromProtoMessage
方法将nweBlock
中的新块数据转换为block
对象 bc.chBlocks <- &block
将block
对象发送到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
}
atomic.LoadInt32(&bc.procInterrupt)
是Go语言中的一个原子操作,用于从变量bc.procInterrupt
的内存地址中加载整数。insertStopped
返回true
表示在调用StopInsert
之后,插入insertStopped
返回true
表示在调用StopInsert
之后,插入操作已经停止。
1.6 InsertChain(*)
func (bc *BlockChain) InsertChain(chain []block2.IBlock) (int, error) {
该函数用于插入区块,是一个很重要的函数
- 首先判断插入的区块长度是否为0,为0就是不合法的区块,就直接返回nil
- 遍历整个chain区块,进行for循环
- 创建变量block和prev分别存储当前一个区块和前一个区块
- 如果区块的编号不是前一个区块编号加1,或者区块的父哈希与前一个区块的哈希不相等,就会输出错误信息
if block.Number64().Cmp(uint256.NewInt(0).Add(prev.Number64(), uint256.NewInt(1))) != 0 || block.ParentHash() != prev.Hash() {
- 初始循环没有错之后,就会上锁,进行插入
bc.lock.Lock()
- 并使用延迟释放锁
defer bc.lock.Unlock()
,确保在函数完成之后会释放锁 - 调用
bc.insertChain(chain)
进行插入(参考1.7)
1.7 insertChain(*)
func (bc *BlockChain) insertChain(chain []block2.IBlock) (int, error) {
这也是一个很重要的函数
- 先判断是否有插入的中断标志
bc.insertStopped()
,如果有,就直接返回0和nil - 创建一些变量:开始时间的计时,头部headers和密封验证标志seals(可以用来在验证的时候选择是否要进行密封验证)等
- 遍历整个要插入的chain,对headers和seals进行赋值(
block.Header()和true
) - 进行验证:调用
bc.engine.VerifyHeaders
对区块的头部和seals进行验证,并返回abort和reaults
其中使用到的函数来自internal/consensus/cpnsensus.go中engine的函数,VerifyHeaders类似于VerifyHeader,但同时验证一批标头。该方法返回一个退出通道以中止操作,并返回一个结果通道以检索异步验证(顺序为输入切片的顺序)。 - 使用延迟函数
defer close(abort)
在函数完成的时候关闭通道 - 创建一个迭代器,调用的是blockchain_insert.go里面的函数
block, err := it.next()
//next返回迭代器中的下一个块,以及该块的任何潜在验证错误。当到达终点时,它将返回(nil,nil)。- 调用
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)如果区块已经被忽略,它会记录忽略的区块数量,并获取下一个区块。
- 在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)
。最后返回当前索引和错误信息。
- 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)
。将返回的结果赋值给变量nopay
和err
。 - 如果发生错误,则返回空指针nil和错误对象err
- 提交事务并返回内部区块状态对象、
nopay
映射表和nil
错误对象。
当区块不为nil并且没有错误或者错误为ErrKnownBlock
的时候,不断遍历blocks:
- 开始计时插入时间
start := time.Now()
,创建变量收据、日志和gas - 通过调用函数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)
并返回 - 验证计时结束
- 得到执行和验证的时间
- 如果调用
evmRecord
的过程中存在err,就返回当前的index和err - 写状态计时开始
wstart := time.Now()
- 调用
status, err = bc.writeBlockWithState(block, receipts, ibs, nopay)
来写块,有错就返回 - 得到写状态时间(15)和插入时间(12)
- 更新queued的数量和uesdgas
- 根据不同的状态执行不同的操作(switch):
- 当状态为CanonStatTy规范链时,会记录插入新块的相关信息,并发送日志事件。如果存在日志
len(logs) > 0
,则发送全局日志事件event.GlobalEvent.Send
。最后将最后一个规范块设置为当前块lastCanon = block
。 - 当状态为SideStatTy侧链时,会记录插入分叉块的相关信息。
- 如果状态既不是CanonStatTy也不是SideStatTy,则会记录插入具有未知状态的块的相关信息,并发出警告日志。
到这里为一整个for循环
- 当block为nil并且错误是
ErrFutureBlock
的时候,我们考虑未来区块
- 将第一个区块加入到未来区块中
bc.AddFutureBlock(block)
,有错的时候就返回 - 获取下一个区块,当区块不为nil并且错误为
ErrUnknownAncestor
的时候,不断遍历获取下一区块 - 将当前区块加入未来区块中
bc.AddFutureBlock(block)
,有错就返回 - 更新queued
- 更新ignored的数量
1.8 insertSideChain
func (bc *BlockChain) insertSideChain(block block2.IBlock, it *insertIterator) (int, error) {
与1.7不同,该函数用于插入侧链
- 创建一些变量用于存储如当前区块和上一个区块等
- 当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.GetTd
和block.ParentHash()
获取父区块的TD值并将将父区块的TD值赋给 externTd。
(2)externTd = *externTd.Add(&externTd, block.Difficulty())
:将当前区块的难度值与父区块的TD值相加,得到当前区块的总难度值,并更新 externTd - 如果区块不在链上的话,就要将区块写上去
(1)记录当前时间作为开始时间
(2)调用bc.WriteBlockWithoutState(block)
方法将区块写入区块链,如果写入过程中出现错误,返回当前索引和错误信息
(2)如果写入成功,记录日志信息 - 更新上一区块,并开始下一轮循环
- 根据
bc.forker.ReorgNeeded
(参考1.15)判断是否需要reorg,如有错就返回当前索引值index和err - 如果不需要reorg,就通过
bc.GetTd
获得难度td并赋值给localtd,输出日志信息并返回当前索引index和err - 如果需要reorg,创建变量hashes和numbers用于存放hash值和区块号
parent := it.previous()
获得前一区块为父区块- 如果父区块不为nil并且检验父区块的状态根不在数据库中
!bc.HasState(parent.StateRoot())
,执行for循环:
- 将父区块的hash和number加入hashes和numbers中
- 通过
bc.GetHeader
向前遍历父区块 - 直到父区块不存在或者父区块的状态根已经存在,此时就找到了分叉点
- 通过上一步得到的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
的哈希值,表示最后一个成功插入的祖先区块的哈希值;另一个是错误信息,如果发生错误则返回非空的错误对象。
- 创建变量存储hash、number和父区块(初始为block)
- 不断向前遍历区块
bc.GetBlock
,将父区块的哈希、区块号依次加入,直到找到具有有效状态的父区块bc.HasState(parent.Hash())
或到达区块链的起始点parent == nil
- 如果找不到有效的父区块,函数将返回一个空的哈希值和一个错误信息
- 函数使用另一个循环来反向遍历祖先区块列表:
- 根据当前哈希值和编号,获取对应的区块对象
bc.GetBlock
,并将其插入到区块链中bc.insertChain
(参考1.7)。如果插入过程中发生错误,函数将返回当前区块的父哈希值和错误信息 - 第一次循环的时候特殊处理,此时要处理的区块为传入的区块block
- 返回最终祖先区块的哈希值和
nil
作为错误信息,表示所有祖先区块已成功插入到区块链中
1.10 WriteBlockWithoutState
func (bc *BlockChain) WriteBlockWithoutState(block block2.IBlock) (err error) {
WriteBlockWithoutState仅将块及其元数据写入数据库,但不写入任何状态。 这用于构建竞争方叉,直到超过规范总难度。
- 如果中止插入
bc.insertStopped()
,就返回errInsertionInterrupted
错误 - 更新数据库数据
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将块和所有关联状态写入数据库
- 上锁和延迟解锁
- 调用
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
。如果发生错误,将返回相应的错误信息。
函数的主要逻辑如下:
- 使用
bc.ChainDB.Update
方法更新区块链数据库,执行以下操作:- 读取父区块的TD
rawdb.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
写入数据库。
- 读取父区块的TD
- 检查是否需要进行重组织(reorg)
bc.forker.ReorgNeeded
(参考1.15),即当前区块是否是头区块的直接子区块。如果是,则进行重组织。 - 如果父级不是头块,则重新组织链
- 在reorg过程中出错的话就返回
NonStatTy
,没出错status就为CanonStatTy
。如果不需要reorg,status就为SideStatTy
- 根据重组织的结果,设置新的头区块
bc.writeHeadBlock
(参考1.13),并保存最新的区块,在这个过程中出错的话就返回NonStatTy
- 如果未来区块中已经存在该区块的哈希值
bc.futureBlocks.Get(block.Hash())
,则从未来区块列表中移除它 - 函数返回写入状态和错误信息
1.13 writeHeadBlock
func (bc *BlockChain) writeHeadBlock(tx kv.RwTx, block block2.IBlock) error {
这段代码是一个名为writeHeadBlock
的函数,它的作用是将一个区块(block)写入区块链数据库。函数接收两个参数:一个是键值对读写事务(tx),另一个是要写入的区块(block)。
- 检查传入的事务是否为空,如果为空,则创建一个新的读写事务
bc.ChainDB.BeginRw(bc.ctx)
并标记为非外部事务notExternalTx = true
,同时使用延迟回滚 - 将区块的哈希值写入数据库两次
rawdb.WriteHeadBlockHash
- 分别写入头部区块哈希和交易查找条目,WriteTxLookupEntries存储块中每个事务的位置元数据,从而实现基于哈希的事务和收据查找
rawdb.WriteTxLookupEntries
- 将区块的哈希值和区块号写入数据库中的规范哈希表
rawdb.WriteCanonicalHash
- 将当前区块设置为传入的区块,并更新头部区块的计数器
- 如果事务不是非外部事务
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的区块号进行不同的比较操作。
- 如果current的区块号大于header的区块号,则返回false,表示不需要重组
- 如果两者的区块号相等,则比较current的难度值是否等于2,如果不等于2,则返回true,表示需要重组;否则返回false
- 如果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())
})
}
该方法的作用是将区块链的头部设置为指定的区块号。
- 根据区块号获得区块
bc.GetBlockByNumber
,有错就返回nil - 使用
bc.ChainDB.Update
函数来更新区块链数据库。在更新过程中,它调用了rawdb包中的rawdb.WriteHeadHeaderHash
函数,将新的头部区块哈希值写入数据库 - 返回更新操作的结果,如果出现错误,则返回该错误。
1.17 AddFutureBlock
func (bc *BlockChain) AddFutureBlock(block block2.IBlock) error {
AddFutureBlock检查该块是否在允许接受的最大窗口内以供将来处理,如果该块太超前且未添加,则返回错误。将一个区块添加到未来区块队列中。
- 计算最大时间戳
max
,它是当前时间加上允许的最大未来区块时间maxTimeFutureBlocks
- 检查传入的区块的时间戳是否大于最大时间戳
block.Time() > max
,如果是,则返回一个错误信息,表示未来区块的时间戳超过了允许的范围。 - 检查传入的区块的难度是否为0
block.Difficulty().Uint64() == 0
,如果是,则不将其添加到未来区块队列中,并返回nil
- 如果时间没有超过最大时间且难度不为0,方法会记录一条日志信息,包括区块的哈希值、区块号、状态根和交易数量。然后,将该区块添加到未来区块队列中
bc.futureBlocks.Add
,并返回nil
2、has判断
2.1 HasBlockAndState
func (bc *BlockChain) HasBlockAndState(hash types.Hash, number uint64) bool {
该函数通过传入的hash和number来判断是否存在对应的区块
- 通过
block := bc.GetBlock(hash, number)
来获取对应的区块block(参考Ⅰ) - 如果获取的block为nil,就返回false,表示不存在这样的区块
- 否则就继续返回调用
bc.HasState(block.Hash())
(参考2.2)判断是否有对应的state
2.2 HasState
func (bc *BlockChain) HasState(hash types.Hash) bool {
- 通过
bc.ChainDB.BeginRo(bc.ctx)
开启一个事务 - 开启过程中有err,就返回false
- 否则使用
rawdb.IsCanonicalHash(tx, hash)
函数来判断有没有,有错err就返回false - 否则就返回在数据库中判断的结果is
- 使用延迟回滚
defer tx.Rollback()
2.3 HasBlock
func (bc *BlockChain) HasBlock(hash types.Hash, number uint64) bool {
该函数通过对应的hash和number来判断是否有对应的区块block
- 创建变量flag来表示最后的结果
- 先在blockcache缓存中进行判断
bc.blockCache.Contains(hash)
,如果存在就返回true - 否则就开始在数据库中查找判断
- 开启
bc.ChainDB.View
,其中调用数据库包rawdb中的rawdb.HasHeader(tx, hash, number)
来进行判断,并将结果赋值给flag - 最后返回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。