区块链技术开发教程

区块链技术指南,区块链技术的工作原理讲解,区块链技术入门教程

以太坊源码分析(六)-带状态的区块写入

2018-11-27

区块链应用技术开发,区块链技术教程,以太坊本文介绍以太坊将一个带状态的区块写入区块链的具体流程 。

点击区块链技术培训课程获取更多区块链技术学习资料。

前文链接:http://www.liankuai.tech/portal/article/index/id/192.html


1 BlockChainWriteBlockWithState方法

1.1 前言


本章节介绍BlockChain的WriteBlockWithState方法。WriteBlockWithState方法的功能是将一个区块写入区块链,同时处理可能发生的分叉。能够执行到WriteBlockWithState这个函数说明区块本身是被验证过的没有问题,所以这个方法一定能将区块写入数据库,但是能不能写入规范链,需要进一步判断,假设写入的是规范链,是在原有规范链基础是追加一个呢? 还是将数据库中的一个分叉升级成规范链呢? 这个也要进一步判断。所以WriteBlockWithState方法将区块写入区块链的同时还会处理可能的分叉。


1.2 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.ErrUnknownAncestor
}
// 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 itself 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{}) // metadata 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 - ethdb.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 haven'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)
             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/btcProcFC.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和区块高度情况

待插入的区块能否插入规范链

待插入的总难度比当前大

可以

待插入的总难度跟当前相同,但待插入的区块高度更低

可以

待插入的总难度跟当前相同,但待插入的区块高度更高

不可以

待插入的总难度跟当前相同,而且待插入的区块高度和当前相同

随机决定

待插入的总难度比当前低

不可以


5reorg变量如果得出是true,表示当前区块是要写入规范链的,首先拿待插入的区块的总难度和当前规范链头区块的总难度比较, 如果待插入的更大,reorgtrue 待插入的区块必然要写入规范链,下面这段代码处理其他几种情况:

//如果待插入的区块总难度和当前相同
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步, reorgtrue,区块一定能写入规范链,那如何确定是将这个区块追加到当前规范链还是将原有一条分叉升级成规范链呢?就是看待插入的区块的父区块是否指向的当前规范链的头区块,如果指向了,就调用BlockChaininsert方法将待插入区块追加到当前规范链,否则调用BlockChainreorg函数将数据库中的一个分叉链升级成规范链。

1.3 insert方法

inert方法就是将区块写入规范链,从下面的代码可以看出将一个区块写入到规范链其实就是3步:

1.      调用WriteCanonicalHash函数将数据库中‘h’ + num + ‘n’标记成待插入区块的hash

2.      调用WriteHeadBlockHash函数将数据库中“LastBlock”标记成待插入区块的hash

3.      调用bc.currentBlock.Store(block)BlockChaincurrentBlock更新成待插入区块


func (bc *BlockChain) insert(block *types.Block) {
// If the block is on a side chain or an unknown one, force other heads onto 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.SetCurrentHeader(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,也就是待插入的区块。


1.4 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 file 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从数据库中删除。


1.5 总结


本章节主要介绍WriteBlockWithState的流程,从上面的分析可以看出WriteBlockWithState函数里面主要是将一个区块写入数据库,然后判断这个区块能不能写入规范链链,如果能写则写入,同时处理分叉。


-END-


001周末班+0628二维码.jpg

区块链应用案例手箭头.png

点击查看更多区块链应用成功案例 ,区块链技术开发教程 。



003学习路径+公众号.jpg



7*24客服电话

150-1118-1611

扫码添加助教微信

二维码
菜单 在线咨询 回到顶部
关闭