核心操作
bucket tree的计算过程可以分为以下四阶段:
- Initialize
- Prepare
- Process
- Commit
Initialize
在初始化阶段,主要进行构建树形态的构建,cache的初始化以及历史数据的恢复(从db中读取最新的根节点的哈希)
所谓树形态的构建就是利用用户配置的capacity及aggreation两个参数构建树的结构。构建函数如下:
- var (
- curlevel int
- curSize int = cap
- levelInfo = make(map[int]int)
- )
- levelInfo[curlevel] = curSize
- for curSize > 1 {
- parSize := curSize / aggr // 根据收敛系数计算下一层的节点个数
- if curSize%aggr != 0 {
- parSize++
- }
- curSize = parSize
- curlevel++
- levelInfo[curlevel] = curSize
- }
- conf.lowest = curlevel
- for k, v := range levelInfo {
- conf.levelInfo[conf.lowest-k] = v // 将每一层的节点个数信息倒置,使得根节点处于0层,哈希桶处于最高层
- }
此外,bucket tree为了(1)增加读取的效率(2)防止写丢失,增加了两个cache用于缓存哈希桶和merkle节点的数据。这两个cache都是利用LRUCache实现的,每次更新时都会同步地更新cache中的内容。如此,下一次bucket tree进行哈希计算时,便可以从cache中命中热数据,尽量避免磁盘的读取。
至于防止写丢失是因为在hyperchain中,validation和commit是两个独立异步的过程,因此在进行完区块100的validation过程时,可能会立刻基于100的状态直接进行区块101的validation。而此时区块100执行过程中对账本的修改还没有被提交的数据库中,因此为了“防止写丢失”,这些内容需要从缓存中命中。
倘若此刻cache发生了容量过小未提交的内容被驱除的情况,就会导致区块101的validation的结果与其他节点不一致(通常为主节点),此时会依赖于RBFT算法进行故障处理。
其中bucketcache是用来存储哈希桶的数据,每一个哈希桶为一个cache数据项。存在的问题是,一个哈希桶本身由若干条数据项组成,随着运行时间增长,一个哈希桶的size会越来越大,导致内存占用量不断上升。
merkleNodeCache是用来存储除最高层以外的所有merkle节点数据的,merkle节点个数是固定的,每个节点的size也是有上限的,因此merkleNodeCache不存在内容占用量变大的问题。
Prepare
在准备阶段,bucket tree会接收由用户传入的修改集,并利用修改集的内容构建脏的哈希桶集。注意,返回的哈希桶中,内部的数据项是按字典序升序排列的。
- func newBuckets(prefix string, entries Entries) *Buckets {
- buckets := &Buckets{make(map[Position]Bucket)}
- for key, value := range entries {
- buckets.add(prefix, key, value)
- }
- for _, bucket := range buckets.data {
- sort.Sort(bucket)
- }
- return buckets
- }
Process
process也就是哈希重计算阶段,可以分为两部分(1)脏哈希桶的哈希重计算(2)脏merkle节点的哈希重计算。
脏哈希桶计算
如上图所示,在bucket tree中新插入两条数据项entry5, entry6。entry5得到的散列地址为Pos2,entry6得到的散列地址为Pos5。
哈希桶计算存在一个合并的操作,即在Pos2,需要将新插入的数据,与历史的数据进行一个合并,且按照固定的排序算法进行重排序,最终得到一个新的哈希桶,包含了所有的新旧数据,且按序排列。
每个哈希桶的哈希值为当前桶中数据的进行哈希计算得到的结果。
如图所示,Pos2与Pos5为两个脏的哈希桶,计算完成之后,将父节点中对应的孩子哈希值置为新的桶哈希值,即Merkle1-1,Merkle1-2为两个脏的merkle节点。
脏merkle节点计算
当哈希桶计算完成之后,便可以进行merkle节点的哈希计算。这步中仅对脏的merkle节点进行哈希重计算。
注意,merkle节点的哈希计算是分层进行的。
每个merkle节点维护其孩子节点的哈希值,若下一层的孩子节点哈希值发生变化,会在之前的计算中,就将最新的哈希值置到父节点中;对于没有发生变化的孩子节点,直接使用历史的哈希值即可。
每个merkle节点的哈希值,为其所有孩子节点哈希值的哈希计算得到。
Commit
计算完成之后,需要将最新的哈希桶数据、merkle节点数据进行持久化。
除此以外,所有的哈希桶数据、merkle节点数据都会被存储到缓存中,对于热数据,既可以提高数据的查找效率,也可以避免数据的写丢失情况。