北大肖臻老师《区块链技术与应用》系列课程学习笔记[14]以太坊-状态树2

前文链接

目录

三、MPT结构

        1.什么是trie结构

        2.Trie结构的特点

        3.Patricia tree

        4.MPT(Merkle Patricia tree)

四、以太坊中的数据结构

        1.块头Block Header的定义

        2.区块Block的定义

        3.External block

扫描二维码关注公众号,回复: 16212536 查看本文章

三、MPT结构

1.什么是trie结构

        Trie:字典树又称前缀树,也是一种(key,value)的树,一般来说key用字符串用的比较多,如将一些单词排成一个trie的数据结构。比如将general,genesis,god,go,good排列成trie的数据结构如下图3-1所示。

图3-1

        图3-1是一个trie的结果,这几个单词都是以G开头的,然后第二个字母就开始分裂了,左边是E,右边是O,左边这前两个单词都是N和E,然后下面再分开,R和S,然后是后三个字母,右边这个分支,O这个分支,Go就已经结束了,从这个可以看到单词可能在trie的中间节点结束,然后左边是D,右边是O,左边变成了God,右边下来是Good。

2.Trie结构的特点

(1)在trie当中,每个节点的分支数目取决于key值里每个元素的取值范围,这个例子当中,每个都是英文单词,而且是大写的,所以每个节点的分叉数目最多是26个,加上一个结束标志位,表示这个单词到这个地方就结束了。那在以太坊中,是什么样的呢?地址是表示成40个十六进制的数,所以分叉数目(有时也叫做Branching Factor)是17,因为是十六进制的0~f,再加上结束标志位,所以是17。

(2)trie的查找速率取决于key的长度,键值越长,查找需要访问的次数就越多。跟上图3-1的例子不同,以太坊中,所有键值都是40,因为地址都是40位十六进制的数,比特币和以太坊的地址是不通用的,两个地址的格式长度都是不一样的(比特币中的地址是用公钥取哈希得到的256位的哈希值),但有一点是类似的,以太坊中的地址也是公钥经过转换得来的,其实就是公钥取哈希,然后前面的不要,只要后面这部分,就得到一个160bit的地址。

(3)只要两个地址不一样,最后肯定映射到树中的不同分支,所以trie是不会出现碰撞的

(4)trie构造的树不受输入排序影响。前面讲Merkle tree,如果不排序的话,一个问题是账户插入到Merkle tree 的顺序不一样,得到的树的结构也不一样。相对于trie,比如这五个单词,换一个顺序插到这个树里面,得到的结果其实是一样的,只要给定的输入不变,无论输入怎么打乱重排,最后插入到trie当中,得到的树是一样的。不同的节点,不论你怎么按照顺序去插入这些账户,最后构造出来的树是一样的。

(5)每次发布一个区块,系统中绝大多数账户的状态是不变的,只有个别受到影响的账户才会变,所以更新操作的局部性很重要。在上面图3-1的例子中,假设要更新genesis这个key对应的value,这个图当中只画出了key,没有画value,只要访问genesis的那个分支,其他分支不用访问的,也不用遍历整棵树,更新的局部性是很好的

(6)缺点:存储浪费。像图3-1中左边分支都只有一个子节点,这种一脉单传的情况,如果能把节点进行合并,可以减小存储的开销,同时也提高了查找的效率,不用一个一个的往下找。由此引入了Patricia tree,也有人写成Patricia trie,就是经过路径压缩的前缀树,即压缩前缀树。

3.Patricia tree

        将图3-1的trie进行路径压缩,结果如图3-2所示,可以看到,G下面还是E和O进行分叉,E下面之后跟的都是EN,再往下就是E和S分叉,然后后面都和在一起了,右边都是一样的。这样压缩之后,直观上看,这个树的高度明显缩短了,访问内存的次数则会大大减少,效率会明显提高。对于Patricia tree来说,新插入一个单词,原来压缩的路径可能需要扩展开,比如这个例子当中,加入geometry,左边的分支就不能这样压缩了。

图3-2

        路径压缩在什么情况下效果比较好?树中插入键值的分布如果是比较稀疏的情况下,做不做路径压缩效果差距比较大,比如说,这个例子当中是用英文单词,比如说每个单词都很长,但是一共没有几个单词,举个例子,比如说有misunderstanding,decentralization(去中心化),disintermediation(非中间化)(intermediaries:中间商)。这三个单词插入到一个普通的trie里面就成了下图3-3所示。可以看到这样的结构效率是比较低的,基本上是一条线了,如果用Patricia tree的话,参考下图3-3所示。

图3-3

       图3-3这样的结构效率是比较低的,用Patricia tree的结果如下图3-4,这个树的高度明显改善。键值分布比较稀疏的时候,路径压缩效果比较好。如果应用在以太坊中,键值则为地址,地址是160位的,所以地址空间有2^{160},这是一个非常非常大的数,如果设计一个计算机程序的算法,需要进行运算的次数是2^{160},那这个在我们的有生之年都不可能算出来,全世界的以太坊的账户数目加在一起也远远没有这么大,跟这个数比,是微乎其微的,所以以太坊的账户是非常非常稀疏的。为什么设计地这么稀疏,不把地址长度缩短一点,这样访问效率也快,也没必要那么稀疏了?以太坊中普通账户的创建方法跟比特币账户的创建方法是一样的,没有中央的节点,每个用户独立创建账户,在本地产生一个公私钥对,就是一个账户。为了防止两个人的账户碰撞(即产生的账户一样,这种可能性是存在的,但是实际发生碰撞的概率是非常小的),特将地址设计得足够长,分布足够稀疏。这个可能看上去有点浪费,但是这是去中心化的系统防止账户冲突的唯一办法,所以是非常稀疏的,这就是为什么在数据结构中,要用Patricia tree。

图3-4

4.MPT(Merkle Patricia tree)

(1)什么是Merkle Patricia tree

        区块链与普通链表的区别:把普通指针换成了哈希指针。Merkel tree和Binary tree的区别:把普通指针换成了哈希指针。在以太坊系统中,将所有账户组织成一个Patricia tree(用路径压缩提高效率),然后把普通指针换成哈希指针,计算出根哈希值,写在Block Header里。比特币的Block Header里只有一个根哈希值,就是区块里包含的交易组成的Merkle tree组成的根哈希值以太坊中有三个,有一个交易组成的交易树、用户状态组成状态树、和收据树,状态树由账户状态组成,他的根哈希值也是写在Block Header里。

(2)根哈希值的作用

防止篡改。根哈希值不发生变化,整个树的任何部分都没有办法被篡改。也就是说,可以使用根哈希值来验证账户的状态是否被篡改过。
②Merkle Proof:证明账户的余额是多少。该账户所在的分支自己向上作Merkle Proof发给轻节点,轻节点可以验证该账户的余额。
验证一个交易是不存在的。给一个地址转账之前,验证一下全节点里有没有这个账户信息,即证明MPT中某个键值是不存在的。证明方法跟Sorted Merkle tree类似,如果存在的话,是在什么样的分支,把这个分支作为Merkle Proof发过去,从而可以证明某个账户信息是不存在的。

(3)以太坊中的MPT——Modified MPT

        以太坊中用到的不是原生的MPT,是Modified MPT,这里对MPT做了一些修改,这些修改并非本质的修改,如图3-5所示,树中的根节点Root取哈希后得到的根哈希值会被写在块头里,如图3-5左上角所示。另,图中各个节点原本存放地址的位置在这里存放的都是哈希值,具体方式参考比特币-思考(1)

图3-5

        每次发布一个新的区块的时候,这个状态树中,有一些节点的值会发生变化,这些改变不是在原地修改,而是新建一个分支,所以说原来的状态实际上是保留下来的,如图3-6所示,以太坊中其实是一个大的MPT里面包含很多小的MPT,每个账户都有一个小的MPT。

图3-6

        系统中每个全节点需要维护的并非一棵MPT,而是每次出现一个区块,都要新建一个MPT,只不过这些状态树中,大部分的节点是共享的,只有少部分发生变化的节点要新建分支。

        为什么要保留历史状态,不能在原地直接修改状态树?系统当中有时候会出现分叉,临时性的分叉是很普遍的,以太坊将出块时间降低到十几秒,导致临时性分叉成为常态(因为区块在网上传播时间可能也需要十几秒)。假设由有个分叉,这两个节点同时获得记账权,上面的分叉胜出,成为最长合法连,下面这个分叉的节点就要回滚(roll back)。也就是说这个节点当前的状态(接受了下面这个节点的状态)要取消,退回到之前的状态,然后沿着上面那条合法链往下推进,如图3-7所示,红色区块所在的链就是需要回滚的。实现回滚呢,就要维护这些历史纪录。

        与比特币不太一样,在比特币系统中,交易类型比较简单,有的时候可以通过反向操作推算出前一个状态。假设存在一个转账交易,A→B(10BTC)。对账户余额的影响就是:A的账户上少了10BTC,B的账户多10BTC,假如这个状态要回滚,就把B账户减少10BTC,A账户加10BTC就行了,简单的转账交易回滚是比较容易的。以太坊中为什么不能直接回滚?因为以太坊中有智能合约,是图灵完备的,编程功能是很强的,从理论上说,可以实现很复杂的功能,跟比特币简单的脚本不太一样,所以以太坊中如果不保存前面的状态,智能合约执行完之后,想在推算出前面是什么状态,这是不可能的,所以想支持回滚,必须保存历史状态。

图3-7

 四、以太坊中的数据结构

1.块头Block Header的定义

图4-1
// Header represents a block header in the Ethereum blockchain.
type Header struct {
    ParentHash  common.Hash    `json:"parentHash"        gencodec:"required"`
    UncleHash   common.Hash    `json:"sha3Uncles"        gencodec:"required"`
    coinbase    common.Address `json:"miner"             gencodec:"required"`
    Root        common.Hash    `json:"stateRoot"         gencodec:"required"`
    TxHash      common.Hash    `json:"transactionsRoot"  gencodec:"required"`
    ReceiptHash common.Hash    `json:"receiptsRoot"      gencodec:"required"`
    Bloom       Bloom          `json:"logsBloom"         gencodec:"required"`
    Difficulty  *big.Int       `json:"difficulty"        gencodec:"required"`
    Number      *big.Int       `json:"number"            gencodec:"required"`
    GasLimit    uint64         `json:"gasLimit"          gencodec:"required"`
    Gasused     uint64         `json:"gasused"           gencodec:"required"`
    Time        *big.Int       `json:"timestamp"         gencodec:"required"`
    Extra       [ ]byte        `json:"extraData"         gencodec:"required"`
    MixDigest   common.Hash    `json:"mixHash"           gencodec:"required"`
    Nonce       BlockNonce     `json:"nonce"             gencodec:"required"`
}

 2.区块Block的定义

图4-2
// Block represents an entire block in the Ethereum blockchain.
type Block struct {
    header       *Header
    uncles       []*Header
    transactions Transactions
    
    //caches
    hash atomic.value
    size atomic.value

    //Td is used by package core to store the total difficulty
    //of the chain up to and including the block.
    td *big.Int

    //These fields are used by package eth to trackl
    //inter-peer block relay.
    ReceivedAt   time.Time
    ReceivedFrom interface{}
}

3.External block

        一个区块真正在网上发布的时候就是发布这些信息,如图4-3所示。

图4-3
type extbllock struct {
    header       *Header
    Txs          []*Transactions
    uncles       []*Header
}

         状态树中保存的是(key,value),key就是地址,目前主要讲的是键值,这个地址的管理方式。value,即账户的状态,是如何存储在状态树当中的?这需要经过一个序列化(RLP:Recursive  Length Prefix)的过程,然后再存储。RLP是一种序列化方法,其特点是简单、极简主义,越简单越好。Protocal buffer:简称Protobuf,是个很有名的做序列化的库。跟这些库相比,RLP的理念就是越简单越好,只支持一种类型,nested array bytes(字节数组),即一个一个字节组成的数组(可以嵌套)。以太坊里的所有的其他类型,如整数和哈希表等,最后都要变成nested array bytes。所以,实现RLP要比实现Protocal buffer简单很多,因为难的东西都不做,都交给应用层做。

猜你喜欢

转载自blog.csdn.net/YSL_Lsy_/article/details/126431674
今日推荐