一、 前言
本章节介绍BlockChain的WriteBlockWithState方法。WriteBlockWithState方法的功能是将一个区块写入区块链,同时处理可能发生的分叉,能够执行到WriteBlockWithState这个函数说明区块本身是被验证过的没有问题,所以这个方法一定能将区块写入数据库,但是能不能写入规范链,需要进一步判断,假设写入的是规范链,是在原有规范链基础是追加一个呢? 还是将数据库中的一个分叉升级成规范链呢? 这个也要进一步判断。所以WriteBlockWithState方法将区块写入区块链的同时还会处理可能的分叉。
二、WriteBlockWithState函数
WriteBlockWithState函数将一个区块写入区块链的过程其实就是将一个区块写入数据库,前面讲BlockChain基本概念的时候提到一个区块在数据库中是几个部分单独存储的,分别是区块头、区块体、总难度、收据、区块号、状态,所以WriteBlockWithState函数就是将区块的这几个部分按特定的键值对写入数据库。
func (bc *BlockChain) WriteBlockWithState(block *types.Block, receipts []*types.Receipt, state *state.StateDB) (status WriteStatus, err error) {
bc.wg.Add(1)
defer bc.wg.Done()
// Calculate the total difficulty of the block
//获取父区块的总难度
ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1)
if ptd == nil {
return NonStatTy, consensus.ErrUnknownAnce[STO](http://www.btb8.com/s/sto/)r
}
// Make sure no inconsistent state is leaked during insertion
bc.mu.Lock()
defer bc.mu.Unlock()
//获取当前规范链头区块的总难度
currentBlock := bc.CurrentBlock()
localTd := bc.GetTd(currentBlock.Hash(), currentBlock.NumberU64())
//计算出待插入区块的总难度
externTd := new(big.Int).Add(block.Difficulty(), ptd)
// Irrelevant of the canonical status, write the block its[ELF](http://www.btb8.com/elf/) to the database
//1将当前区块的总难度写入数据库
//使用 "h" num has "t"作为key
if err := bc.hc.WriteTd(block.Hash(), block.NumberU64(), externTd); err != nil {
return NonStatTy, err
}
// Write other block data using a batch.
batch := bc.db.NewBatch()
//2使用数据库的Write接口将block(header, body)写入到数据库中
//写header使用"h" num has 作为key
//写body使用"b" num has作为key
rawdb.WriteBlock(batch, block)
//3将新的状态数树内容写入到数据库
root, err := state.Commit(bc.chainConfig.IsEIP158(block.Number()))
if err != nil {
return NonStatTy, err
}
triedb := bc.stateCache.TrieDB()
// If we're running an archive node, always flush
if bc.cacheConfig.Disabled {
if err := triedb.Commit(root, false); err != nil {
return NonStatTy, err
}
} else {
// Full but not archive node, do proper garbage collection
triedb.Reference(root, common.Hash{}) // met[ADA](http://www.btb8.com/ada/)ta reference to keep trie alive
bc.triegc.Push(root, -float32(block.NumberU64()))
if current := block.NumberU64(); current > triesInMemory {
// If we exceeded our memory allowance, flush matured singleton nodes to disk
var (
nodes, imgs = triedb.Size()
limit = common.StorageSize(bc.cacheConfig.TrieNodeLimit) * 1024 * 1024
)
if nodes > limit || imgs > 4*1024*1024 {
triedb.Cap(limit - [ETH](http://www.btb8.com/eth/)db.IdealBatchSize)
}
// Find the next state trie we need to commit
header := bc.GetHeaderByNumber(current - triesInMemory)
chosen := header.Number.Uint64()
// If we exceeded out time allowance, flush an entire trie to disk
if bc.gcproc > bc.cacheConfig.TrieTimeLimit {
// If we're exceeding limits but ha[VEN](http://www.btb8.com/ven/)'t reached a large enough memory gap,
// warn the user that the system is becoming unstable.
if chosen < lastWrite triesInMemory && bc.gcproc >= 2*bc.cacheConfig.TrieTimeLimit {
log.Info("State in memory for too long, committing", "time", bc.gcproc, "allowance", bc.cacheConfig.TrieTimeLimit, "optimum", float64(chosen-lastWrite)/triesInMemory)
}
// Flush an entire trie and restart the counters
triedb.Commit(header.Root, [TRUE](http://www.btb8.com/true/))
lastWrite = chosen
bc.gcproc = 0
}
// Garbage collect anything below our required write retention
for !bc.triegc.Empty() {
root, number := bc.triegc.Pop()
if uint64(-number) > chosen {
bc.triegc.Push(root, number)
break
}
triedb.Dereference(root.(common.Hash), common.Hash{})
}
}
}
//4收据内容写入到数据库
//使用 "r" num hash 作为key, receipts列表的RLP编码值作为value
rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receipts)
//5将待插入的区块写入规范链
// If the total difficulty is higher than our known, add it to the canonical chain
// Second clause in the if statement reduces the vulnerability to selfish mining.
// Please refer to http://www.cs.cornell.edu/~ie53/publications/[BTC](http://www.btb8.com/btc/)ProcFC.pdf
//如果待插入区块的总难度等于本地规范链的总难度,但是区块号小于当前规范链的头区块号, 待插入的区块所在的分叉更有效,需要处理分叉,并更新规范连
//如果待插入区块的总难度等于本地规范链的总难度,但是区块号等于当前规范链的头区块号,随机决定哪个条链是规范链
//如果待插入区块的总难度大于本地规范链的总难度,那Block必定要插入规范连
//如果待插入区块的总难度小于本地规范链的总难度, 待插入区块在另一个分叉上,不需要插入
reorg := externTd.Cmp(localTd) > 0
currentBlock = bc.CurrentBlock()
if !reorg && externTd.Cmp(localTd) == 0 {
// Split same-difficulty blocks by number, then at random
reorg = block.NumberU64() < currentBlock.NumberU64() || (block.NumberU64() == currentBlock.NumberU64() && mrand.Float64() < 0.5)
}
//6如果reorg为true,说明一定是写入规范链,接着判断是不是产生分叉
if reorg {
// Reorganise the chain if the parent is not the head block
if block.ParentHash() != currentBlock.Hash() {
//分叉处理
if err := bc.reorg(currentBlock, block); err != nil {
return NonStatTy, err
}
}
// Write the positional metadata for transaction/receipt lookups and preimages
rawdb.WriteTxLookupEntries(batch, block)
rawdb.WritePreimages(batch, block.NumberU64(), state.Preimages())
status = CanonStatTy
} else {
status = SideStatTy
}
//将数据缓冲写入数据库
if err := batch.Write(); err != nil {
return NonStatTy, err
}
// Set new head.
if status == CanonStatTy {
//如果这个区块可以插入本地规范连, 就将它插入本地规范链
bc.insert(block)
}
//如果futureBlock中存在刚插入的区块, 就将它删除
bc.futureBlocks.Remove(block.Hash())
return status, nil
}
第5步是看这个区块能不能写入规范链,前面我们提到一个区块能不能写入规范链得看这个区块的总难度是不是大于当前的规范链头区块的总难度,那如果两者难度相同呢,怎么办? 以太坊的做法是再由区块高度来比较,所以存在下面几种可能性以及待插入区块是否能插入规范链:
TD和区块高度情况--------待插入的区块能否插入规范链
待插入的总难度比当前大-------可以
待插入的总难度跟当前相同,但待插入的区块高度更低---------可以
待插入的总难度跟当前相同,但待插入的区块高度更高---------不可以
待插入的总难度跟当前相同,而且待插入的区块高度和当前相同----随机决定
待插入的总难度比当前低----不可以
第5的reorg变量如果得出是true,表示当前区块是要写入规范链的,首先拿待插入的区块的总难度和当前规范链头区块的总难度比较, 如果待插入的更大,reorg为true, 待插入的区块必然要写入规范链,下面这段代码处理其他几种情况:
//如果待插入的区块总难度和当前相同
if !reorg && externTd.Cmp(localTd) == 0 {
// Split same-difficulty blocks by number, then at random
//另外再比较区块高度,如果待插入的总难度跟当前相同,但待插入的区块高度低,reorg也为true,如果区块高度也相同则随机决定。mrand.Float64()产生一个随机数在0~1之间的浮点数
reorg = block.NumberU64() < currentBlock.NumberU64() || (block.NumberU64() == currentBlock.NumberU64() && mrand.Float64() < 0.5)
}
第6步, reorg为true,区块一定能写入规范链,那如何确定是将这个区块追加到当前规范链还是将原有一条分叉升级成规范链呢?就是看待插入的区块的父区块是否指向的当前规范链的头区块,如果指向了,就调用BlockChain的insert方法将待插入区块追加到当前规范链,否则调用BlockChain的reorg函数将数据库中的一个分叉链升级成规范链。
三、 Insert方法
inert方法就是将区块写入规范链,从下面的代码可以看出将一个区块写入到规范链其实就是3步:
1. 调用WriteCanonicalHash函数将数据库中‘h’ num ‘n’标记成待插入区块的hash。
2. 调用WriteHeadBlockHash函数将数据库中“LastBlock”标记成待插入区块的hash。
3. 调用bc.currentBlock.Store(block)将BlockChain的currentBlock更新成待插入区块
func (bc *BlockChain) insert(block *types.Block) {
// If the block is on a side chain or an unknown one, force other heads [ONT](http://www.btb8.com/ont/)o it too
//1读出待插入block的区块号对应在规范连上的区块hash值, 与block的hash值对对比, 看是否相等,相当于判断当前HeadChain是否正确
updateHeads := rawdb.ReadCanonicalHash(bc.db, block.NumberU64()) != block.Hash()
// Add the block to the canonical chain number scheme and mark as the head
//2更新规范连上block.number的hash值为 block.hash
rawdb.WriteCanonicalHash(bc.db, block.Hash(), block.NumberU64())
//3正式将区块写入规范连
//更新数据库中的“LastBlock”
rawdb.WriteHeadBlockHash(bc.db, block.Hash())
//将BlockChain中的currentBlock替换成block
bc.currentBlock.Store(block)
// If the block is better than our head or is on a different chain, force update heads
if updateHeads {
//4将headChain的头设置成待插入规范连的区块头, BLockChain和HeadChain再次齐头并进
bc.hc.S[ETC](http://www.btb8.com/etc/)urrentHeader(block.Header())
rawdb.WriteHeadFastBlockHash(bc.db, block.Hash())
bc.currentFastBlock.Store(block)
}
}
上面代码中updateHeads如果为true说明headerChain的延伸发生错误,由于headerChain在更新一个头区块的时候也要更新数据库中‘h’ num ‘n’,所以insert函数只要从数据库中读出待插入区块号在数据库中‘h’ num ‘n’标记的hash,是不是跟待插入区块的hash相同,如果不同,说明headerChain延伸错误,需要纠正回来,所谓的纠正就是将headerChain的头更新成BlockChain的currentBlock,也就是待插入的区块。
四、Reorg方法
reorg函数功能是处理区块插入时引起的分叉。也就是说待插入区块是插入规范链,但是它的父区块指向的又不是当前规范链的头区块,说明数据中的一个分叉链要升级成规范链。reorg的原理是向下追溯,找到这两条链的共同祖先区块,这个共同祖先区块就是分叉点,然后将新链(待插入区块所在的链)上从分叉点后面所有区块全部重新调用上面的insert方法更新一下规范链,这样原来的规范链就变成了一条分叉链。待插入区块所在的链变成了新的规范链。
func (bc *BlockChain) reorg(oldBlock, newBlock *types.Block) error {
var (
newChain types.Blocks
oldChain types.Blocks
commonBlock *types.Block
deletedTxs types.Transactions
deletedLogs []*types.Log
// collectLogs collects the logs that were generated during the
// processing of the block that corresponds with the given hash.
// These logs are later announced as deleted.
collectLogs = func(hash common.Hash) {
// Coalesce logs and set 'Removed'.
number := bc.hc.GetBlockNumber(hash)
if number == nil {
return
}
receipts := rawdb.ReadReceipts(bc.db, hash, *number)
for _, receipt := range receipts {
for _, log := range receipt.Logs {
del := *log
del.Removed = true
deletedLogs = append(deletedLogs, &del)
}
}
}
)
//1找到新链和原来规范链的共同祖先
// first reduce whoever is higher bound
if oldBlock.NumberU64() > newBlock.NumberU64() {
//如果老分支比新分支高, 就减少老分支
// reduce old chain
for ; oldBlock != nil && oldBlock.NumberU64() != newBlock.NumberU64(); oldBlock = bc.GetBlock(oldBlock.ParentHash(), oldBlock.NumberU64()-1) {
oldChain = append(oldChain, oldBlock)
deletedTxs = append(deletedTxs, oldBlock.Transactions()...)
collectLogs(oldBlock.Hash())
}
} else {
//如果新分支比老分支高, 就减少新分支
// reduce new chain and append new chain blocks for inserting later on
for ; newBlock != nil && newBlock.NumberU64() != oldBlock.NumberU64(); newBlock = bc.GetBlock(newBlock.ParentHash(), newBlock.NumberU64()-1) {
newChain = append(newChain, newBlock)
}
}
if oldBlock == nil {
return fmt.Errorf("Invalid old chain")
}
if newBlock == nil {
return fmt.Errorf("Invalid new chain")
}
//找到共同祖先
for {
if oldBlock.Hash() == newBlock.Hash() {
commonBlock = oldBlock
break
}
oldChain = append(oldChain, oldBlock)
newChain = append(newChain, newBlock)
deletedTxs = append(deletedTxs, oldBlock.Transactions()...)
collectLogs(oldBlock.Hash())
oldBlock, newBlock = bc.GetBlock(oldBlock.ParentHash(), oldBlock.NumberU64()-1), bc.GetBlock(newBlock.ParentHash(), newBlock.NumberU64()-1)
if oldBlock == nil {
return fmt.Errorf("Invalid old chain")
}
if newBlock == nil {
return fmt.Errorf("Invalid new chain")
}
}
// Ensure the user sees large reorgs
if len(oldChain) > 0 && len(newChain) > 0 {
logFn := log.Debug
if len(oldChain) > 63 {
logFn = log.Warn
}
logFn("Chain split detected", "number", commonBlock.Number(), "hash", commonBlock.Hash(),
"drop", len(oldChain), "dropfrom", oldChain[0].Hash(), "add", len(newChain), "addfrom", newChain[0].Hash())
} else {
log.Error("Impossible reorg, please [FIL](http://www.btb8.com/fil/)e an issue", "oldnum", oldBlock.Number(), "oldhash", oldBlock.Hash(), "newnum", newBlock.Number(), "newhash", newBlock.Hash())
}
//2将新链插入到规范链中, 同时收集插入到规范连的所有交易
// Insert the new chain, taking care of the proper incremental order
//将新链插入到规范链中
var addedTxs types.Transactions
for i := len(newChain) - 1; i >= 0; i-- {
// insert the block in the canonical way, re-writing history
bc.insert(newChain[i])
// write lookup entries for hash based transaction/receipt searches
//把所有新分支的区块交易查询入口插入到数据库中
rawdb.WriteTxLookupEntries(bc.db, newChain[i])
addedTxs = append(addedTxs, newChain[i].Transactions()...)
}
// calculate the difference between deleted and added transactions
//3找出待删除列表中的那些不在待添加的交易列表的交易,并从数据库中删除那些交易查询入口
diff := types.TxDifference(deletedTxs, addedTxs)
// When transactions get deleted from the database that means the
// receipts that were created in the fork must also be deleted
for _, tx := range diff {
rawdb.DeleteTxLookupEntry(bc.db, tx.Hash())
}
//4向外发送区块被重新组织的事件,向外发送日志删除的事件
if len(deletedLogs) > 0 {
go bc.rmLogsFeed.Send(RemovedLogsEvent{deletedLogs})
}
if len(oldChain) > 0 {
go func() {
for _, block := range oldChain {
bc.chainSideFeed.Send(ChainSideEvent{Block: block})
}
}()
}
return nil
}
上面的代码还涉及到一个概念叫交易查找入口(TxLookupEntry),所谓的交易查找入口就是在数据库中记录了所有规范链上区块中每一个交易的信息,信息的数据结构如下:
type TxLookupEntry struct {
BlockHash common.Hash
BlockIndex uint64
Index uint64
}
存储这个一个结构有很大的意义,如果你知道一个交易的hash,这个hash就能在数据库中查找有没有这个查找入口,如果没有,说明交易没有上链,如果有,就能快速确定这个交易在哪个区块中,已经在区块中的偏移。存储的数据结构包括3部分内容,这个交易所在的区块的hash(BlockHash)和区块号(BlockIndex)、交易在区块中的偏移(Index)。在数据库中存储的时候是以这个交易的hash值编码后的结果为key,这个数据结构编码后的结果为value存储的。当然只有规范链上的区块中的交易会存储,分叉链上的区块中的交易是不会存储的。所以reorg函数中在回溯老链和新链的时候会把两条链上所有的交易收集起来,然后如同第3步找出那些老链中不存在于新链的交易,将他们的TxLookupEntry从数据库中删除。
五、 总结
本章节主要介绍WriteBlockWithState的流程,从上面的分析可以看出WriteBlockWithState函数里面主要是将一个区块写入数据库,然后判断这个区块能不能写入规范链链,如果能写则写入,同时处理分叉。
网友评论