在以太坊中,一种经过改良的默克尔树非常关键,是以太坊数据安全与效率的保障,此树在以太坊中称之为 MPT(默克尔压缩前缀树)。 MPT 全称是 Merkle Patricia Trie 也叫 Merkle Patricia Tree,是 Merkle Tree 和 Patricia Tree 的混合物。 Merkle Tree(默克尔树) 用于保证数据安全,Patricia Tree(基数树,也叫基数特里树或压缩前缀树) 用于提升树的读写效率。
以太坊不同于比特币的 UXTO 模型,在账户模型中,账户存在多个属性(余额、代码、存储信息),属性(状态)需要经常更新。因此需要一种数据结构来满足几点要求:
要求①是默克尔树特性,但要求②③则非默克尔树的优势。 对于要求②,可将数据 Key 进行一次哈希计算,得到确定长度的哈希值参与树的构建。而要求③则是引入位置确定的压缩前缀树并加以改进。
对部分开发者来说 Trie 这个术语可能比较陌生,而理解 Trie 的概念在本文中非常重要。因此,我先介绍 Trie 。
在计算机科学中, Trie ,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。 与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。 一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。 一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
Trie 这个术语来自于re Trie val。根据词源学, Trie 的发明者Edward Fredkin把它读作/ˈtriː/ "tree"。但是,其他作者把它读作/ˈtraɪ/ "try"。如图所示:
在图示中,键标注在节点中,值标注在节点之下。每一个完整的英文单词对应一个特定的整数。 Trie 可以看作是一个确定有限状态自动机,尽管边上的符号一般是隐含在分支的顺序中的。键不需要被显式地保存在节点中。图示中标注出完整的单词,只是为了演示 Trie 的原理。
Trie 中的键通常是字符串,但也可以是其它的结构。 Trie 的算法可以很容易地修改为处理其它结构的有序序列,比如一串数字或者形状的排列。比如,bitwise Trie 中的键是一串比特,可以用于表示整数或者内存地址。
上文摘自维基百科。
在压缩前缀树(基数树)中,键值是通过树到达相应值的实际路径值。 也就是说,从树的根节点开始,键中的每个字符会告诉您要遵循哪个子节点以获取相应的值,其中值存储在叶节点中,叶节点终止了穿过树的每个路径。假设键是包含 N 个字符的字母,则树中的每个节点最多可以有 N 个子级,并且树的最大深度是键的最大长度。
虽然基数树使得以相同字符序列开头的键的值在树中靠得更近,但是它们可能效率很低。 例如,当你有一个超长键且没有其他键与之共享前缀时,即使路径上没有其他值,但你必须在树中移动(并存储)大量节点才能获得该值。 这种低效在以太坊中会更加明显,因为参与树构建的 Key 是一个哈希值有 64 长(32 字节),则树的最长深度是 64。树中每个节点必须存储 32 字节,一个 Key 就需要至少 2KB 来存储,其中包含大量空白内容。 因此,在经常需要更新的以太坊状态树中,优化改进基数树,以提高效率、降低树的深度和减少 IO 次数,是必要的。
为了解决基数树的效率问题,以太坊对基数树的最大改动是丰富了节点类型,围绕不同节点类型的不同操作来解决效率。
多种节点类型的不同操作方式,虽然提升了效率,但复杂度被加大。而在 geth 中,为了适应实现,节点类型的设计稍有不同:
//trie/node.go:35
type (
fullNode struct { //分支节点
Children [17]node
flags nodeFlag
}
shortNode struct { //短节点:叶子节点、扩展节点
Key []byte
Val node
flags nodeFlag
}
hashNode []byte //哈希节点
valueNode []byte //数据节点,但他的值就是实际的数据值
)
var nilValueNode = valueNode(nil) //空白节点
16
时表示为叶子节点。当 shortNode 是叶子节点是,Val 是 valueNode。在改进过程中,为适应不同场景应用,以太坊定义了几种不同类型的 key 。
下图是 key 有特定的使用场景,基本支持逆向编码,在下面的讲解中 Key 在不同语义下特指的类型有所不同。
当我们把一组数据(romane、romanus、romulus、rubens、ruber、rubicon、rubicunds)写入基数树中时,得到如下一颗基数树:
在上图的基数树中,持久化节点,有 13 次 IO。数据越多时,节点数越多,IO 次数越多。另外当树很深时,可能需要遍历到树的底部才能查询到数据。 面对此效率问题,以太坊在树中加入了一种名为分支节点(branch node) 的节点结构,将其子节点直接包含在自身的数据插槽中。
这样可缩减树深度和减少IO次数,特别是当插槽中均有子节点存在时,改进效果越明显。 下图是上方基数树在采用分支节点后的树节点逻辑布局:
从图中可以看出节点数量并无改进,仅仅是改变了节点的存放位置,节点的分布变得紧凑。图中大黑圆圈均为分支节点,它包含一个或多个子节点, 这降低了 IO 和查询次数,在上图中,持久化 IO 只有 6 次,低于基数树的 12 次。
这是因为在持久化分支节点时,并不是将叶子节点分开持久化,而是将其存储在一块。并将持久化内容的哈希值作为一个新节点来参与树的进一步持久化,这种新型的节点称之为扩展节点。比如,数据 rubicon(6) 和 rubicunds(7) 是被一起持久化,在查询数据 rubicon 时,将根据 hasNode 值从数据库中读取分支节点内容,并解码成分支节点,内含 rubicon 和 rubicunds。
另外,数据 Key 在进入 MPT 前已转换 Secure Key。 因此,key 长度为 32 字节,每个字节的值范围是[0 - 255]。 如果在分支节点中使用 256 个插槽,空间开销非常高,造成浪费,毕竟空插槽在持久化时也需要占用空间。同时超大容量的插槽,也会可能使得持久化数据过大,可能会造成读取持久化数据时占用过多内存。 如果将 Key 进行Hex 编码,每个字节值范围被缩小到 [0-15] 内(4bits)。这样,分支节点只需要 16 个插槽来存放子节点。
上图中 0 - f 插槽索引是半字节值,也是 Key 路径的一部分。虽然一定程度上增加了树高,但降低了分支节点的存储大小,也保证了一定的分支节点合并量。
上面已介绍树的改进内容,下面我们来讲解经过改进后,树的增删改查将有哪些不一样。在讲解前需要你掌握几个内含的规则和概念:
即使在修改某项数据时,也会涉及到查询,因此我先讲讲 MPT 树的查询逻辑。 在树中查找数据,需要考虑性能,省时和低 IO 是关键。 因为数据项 [Key,Value] 的 Key 是确定的,那么数据在 MPT 中的树路径也是确定的。 因此,达到数据项的路径是唯一的,是效率最高的最短路径查找。
下图是以太坊从 MPT 中查询查找某 Key 对应数据的流程图。
图中流程中,有几点需要稍微说明:
插入数据包含新数据加入和旧数据修改。下图是往 MPT 中插入数据流程。
首先,插入空节点是无意义的,反而会使树变得臃肿。 因此,当数据为空时,需从树中删除该数据节点。 否则需要根据数据节点的路径从树根开始查找到数据节点所在位置来更新节点。在查找时根据路径中节点的类型不同,需要不同的解析方式。
插入数据的过程,是深度递归遍历方式。先深度查找,抵达数据应处位置,再从下向上依次更新此路径上的节点。 虽然只更新了路径中的相关节点,但这毕竟涉及多个节点的更新。从这一点上看,MPT 性能并不出色。
从 MPT 中删除数据节点,这比插入数据更加复杂。从树中删除一个节点是容易的,但在 MPT 中删除节点后需要根据前面的改进方案调整结构。 比如,原本是一个分支节点下有两个子节点,现在删除一个子节点后,只有一个子节点的分支节点的存储是无意义的,需要移除并将剩余的子节点上移。 下图是 MPT 中删除数据的流程图。
同样,删除数据也是深度递归遍历。先深度查找,抵达数据应处位置,再从下向上依次更新此路径上的节点。 在删除过程中,主要是对删除后节点的调整。有两个原则:
删除数据也涉及路径上节点的更新,图中的绿色虚线是表示递归删除节点。
下面,我演示依次将一组数据 romane、romanus、romulus、rubens、ruber、rubicon、rubicunds 插入到 MPT 中时的树结构的变化情况。
首先依次写入:romane、romanus、romulus 后树的变化如下:
图中的每一个圆圈均代表一个节点,只是节点的类型不同。需要注意的是,图中的红色字部分,实际是一个短节点(shortNode)。 比如,红色的“roman“ 短节点的 key 为 roman, value 是分支节点。继续写入 rubens、ruber、rubicon 的变化过程如下:
最后,写入最后一个数据项 rubicunds 后可得到最终的 MPT 树结构:
注意,本过程演示中,为降低复杂度,省去了 key 的 Secure 和 Hex 过程。
即使以太坊有大量改进基数树,形成 MPT。但还是并没有解决树节点更新时的蝴蝶效应问题。 在 MPT 中树的最大深度是 64,当树充分大时,为更新一个数据节点而需要连带更新的节点也非常多。 这使得以太坊的数据更新是昂贵的。大量的变动也会使得每产生一个新区块,持久化数据后。 有大量的存储不再属于最新状态的一部分,即以太坊的数据增量更新的体量依旧很大。
如果要满足以太坊 2.0 的性能要求,继续改进 MPT 是不可忽略的。
上面已描述 MPT 在内存中的结构,下面我们以 romane、romanus、romulus 为示例数据,来讲解是何如计算出 MPT 树的一个树根 Root 值的。
上图是三项数据的业务 Key 经过 HP 编码后,写入 MPT 树后所形成的 MPT 树结构布局。HP表和树的虚线连接表示树路径的生成依据,这是根据前面所描述的 MPT 生成规则而形成的树结构。在树中,一共有6 个节点,其中节点 1 和 3 为扩展节点,节点 2 和 5 为分支节点,节点 4 和 6 为叶子节点。可以看到在分支节点 5 中的 value 位置存放着业务 key “romane” 所对应的值“罗马”,但业务 key “romanus”和“romulus” 则存放在独立的叶子节点中。
当我们执行 trie.Commit 时将获得一个 Hash 值,称之为 树的 Root 值,这个 Root 值是如何得到的呢? Root 值算法源自默克尔树(Merkle Tree),在默克尔树中,树根值是由从子叶开始不断进行哈希计算得到最终能代表这棵树数据的哈希值。
同样在计算 MPT 树的 Root 值是也是如此,在 MPT 中一个节点的哈希值,是节点内容经 RLP 编码后的 Keccak256 哈希值。当对一个节点进行哈希计算遵循如下规则:
根据上面的规则,我们可以得到上面 MPT 树进行哈希计算时的节点分布:
图中,可哈希的节点只有 4 个,而叶子节点 4 和 6 则直接属于分支节点的一部分参与哈希计算。MPT 的 Root 值是 MPT 树根节点的哈希值。在本示例中,Root 节点为 节点 1,Hash(节点 1)=0x84f3c5......80ef13
即为 MPT 的 Root 值。扩展节点 1 和 3 的 key 在哈希计算时有进行 HP 编码。需要编码的原因是为了区分扩展节点的 value 是叶子节点还是分支节点的区别,具体见 HP 编码规则。
当需要将 MPT Commit 到 DB 时,这颗树的数据是如何完整存储到数据库的呢?以太坊的持久层是 KV 数据库,一个 Key 对应一份存储内容。 当上面在计算 Root 值时,实际上已完成了哈希值和节点内容的处理过程。不同于在内存中计算 Root 值,在持久化时是持久化和 Hash 计算同步进行。
从Root的计算规则可知,HASH 计算是递归的,从树的叶子节点向上计算。每次计算出一个节点哈希时,将使用此哈希值作为数据库的 Key,存放节点的 RLP 持久化内容到数据库中。 因为节点哈希值以及被包含在父节点中,直至树根。因此,我们只需要知道一颗树的 Root 值,便可以依次从 DB 中读取节点数据,并在内存中构建完整的 MPT 树。
比如,上图中的树的 Root 值为 0x84f3c5……80ef13
,通过 db.Get(root)
可获得节点 1,在通过 db.Get(node1.Value)
可获得节点 2...,直至节点 5。
需要注意的是,在分支节点 2 中,他的子节点插槽 6 的位置上是记录的节点 3 的哈希值。在从数据库中获得节点 2 时,并不会立刻将 节点 3 立刻加载。而是在需要使用 到 node2.Children[6]时,根据 node2.Children[6].Value 的类型仅一步判断。如果它是 hashNode 类型,则将根据此 Value 获取节点数据并展开。这样,保证在使用树时是按需实例化节点。
这并非 MPT 树的必要部分,是为了解决路径深度攻击而将数据进入 MPT 前进行一次安全清洗,使用 Keccak256(key) 得到的key 的哈希值替换原数据 key。
在实现上,只需要在原 MPT 树进行依次封装即可获得一颗 Secure MPT 树。
用于树路径中,是将数据 key 进行半字节拆解而成。即依次将 key[0],key[1],...,key[n] 分别进行半字节拆分成两个数,再依次存放在长度为 len(key)+1 的数组中。
并在数组末尾写入终止符 16
。算法如下:
半字节,在计算机中,通常将8位二进制数称为字节,而把4位二进制数称为半字节。 高四位和低四位,这里的“位”是针对二进制来说的。比如数字 250 的二进制数为 11111010,则高四位是左边的 1111,低四位是右边的 1010。
// trie/encoding.go:65
func keybytesToHex(str []byte) []byte {
l := len(str)*2 + 1
var nibbles = make([]byte, l)
for i, b := range str {
nibbles[i*2] = b / 16
nibbles[i*2+1] = b % 16
}
nibbles[l-1] = 16
return nibbles
}
例如:字符串 “romane” 的 bytes 是 [114 111 109 97 110 101]
,在 HEX 编码时将其依次处理:
i | key[i] | key[i]二进制 | nibbles[i*2]=高四位 | nibbles[i*2+1]=低四位 |
---|---|---|---|---|
0 | 114 | 011100102 | 01112= 7 | 00102= 2 |
1 | 111 | 011011112 | 01102=6 | 11112=15 |
2 | 109 | 011011012 | 01102=6 | 11012=13 |
3 | 97 | 011000012 | 01102=6 | 00012=1 |
4 | 110 | 011011102 | 01102=6 | 11102=14 |
5 | 101 | 011001012 | 01102=6 | 01012=5 |
最终得到 Hex("romane") = [7 2 6 15 6 13 6 1 6 14 6 5 16]
Hex-Prefix 编码是一种任意量的半字节转换为数组的有效方式,还可以在存入一个标识符来区分不同节点类型。 因此 HP 编码是在由一个标识符前缀和半字节转换为数组的两部分组成。存入到数据库中存在节点 Key 的只有扩展节点和叶子节点,因此 HP 只用于区分扩展节点和叶子节点,不涉及无节点 key 的分支节点。其编码规则如下图:
前缀标识符由两部分组成:节点类型和奇偶标识,并存储在编码后字节的第一个半字节中。 0 表示扩展节点类型,1 表示叶子节点,偶为 0,奇为 1。最终可以得到唯一标识的前缀标识:
当偶长度时,第一个字节的低四位用0
填充,当是奇长度时,则将 key[0] 存放在第一个字节的低四位中,这样 HP 编码结果始终是偶长度。
这里为什么要区分节点 key 长度的奇偶呢?这是因为,半字节 1
和 01
在转换为 bytes 格式时都成为<01>
,无法区分两者。
例如,上图 “以太坊 MPT 树的哈希计算”中的控制节点1的key 为 [ 7 2 6 f 6 d]
,因为是偶长度,则 HP[0]= (00000000) 2=0,H[1:]= 解码半字节(key)。
而节点 3 的 key 为 [1 6 e 6 5]
,为奇长度,则 HP[0]= (0001 0001)2=17。
下面是 HP 编码算法的 Go 语言实现:
// trie/encoding.go:37
func hexToCompact(hex []byte) []byte {
terminator := byte(0)
if hasTerm(hex) {
terminator = 1
hex = hex[:len(hex)-1]
}
buf := make([]byte, len(hex)/2+1)
buf[0] = terminator << 5 // the flag byte
if len(hex)&1 == 1 {
buf[0] |= 1 << 4 // odd flag
buf[0] |= hex[0] // first nibble is contained in the first byte
hex = hex[1:]
}
decodeNibbles(hex, buf[1:])
return buf
}
func decodeNibbles(nibbles []byte, bytes []byte) {
for bi, ni := 0, 0; ni < len(nibbles); bi, ni = bi+1, ni+2 {
bytes[bi] = nibbles[ni]<<4 | nibbles[ni+1]
}
}
在 Go 语言中因为叶子节点的末尾字节必然是 16(Hex 编码的终止符),依据此可以区分扩展节点还是叶子节点。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。