2.1 密码学
密码学知识被广泛用于安全的信息通信、数据存储、交易验证等方面,也是区块链最基础的技术之一。这些知识包括对信息的转换、加解密,以及校验过程,也包括以太坊地址和交易Hash,交易信息RLP编码、基于椭圆曲线公私钥签名、区块Merkle树交易等。
2.1.1 Hash
Hash,在数学上也被称为“散列”,把任意长度的输入,通过Hash算法,变换成固定长度的输出,该输出就是Hash值。这种转换其实是一种压缩映射,也就是,Hash值的空间通常远小于输入的空间,稍微不同的输入,可能产生差异很大的输出值,当然不同的输入有时候也可能产生相同的输出,出现相同输出值的概率就叫Hash的碰撞率。本质上Hash是一种将不同信息通过一种恰当的方法产生消息摘要,便于在后续使用时得到较好的识别。见图2-1。
图2-1 Hash数学模型
Hash函数的实现,经过多年的发展已经有非常多的种类和实现,有的强调实现简单快速,有的注重Hash结果的较小的碰撞率,还有的关注算法复杂实现较高的安全性,总之要根据不同的应用场景,选择适当的Hash算法。一些知名的Hash函数包括MD5、SHA系列,以及PBKDF2等。
SHA(Secure Hash Algorithm, SHA)家族的系列Hash算法,是由美国国家安全局(NSA)设计,并由美国国家标准与技术研究院(NIST)发布,在全球范围内被广泛使用的安全杂凑算法,常常应用在数字签名和校验的场景中,其中尤以SHA1使用非常广泛。但是,2005年2月,山东大学王小云等发表了完整版对SHA-1的攻击,只需少于2的69次方的计算复杂度,就能找到一组碰撞,根据摩尔定律,随着计算机计算能力的提高,SHA1很快就被认为一种不安全的Hash算法。这之后,NIST又相继发布了SHA-224、SHA-256、SHA-384和SHA-512,后四者都称为SHA-2; SHA-256和SHA-512是很新的杂凑函数,实际上二者结构是相同的,只在循环执行的次数上有所差异。SHA-224和SHA-384则是前述二种杂凑函数的截短版,利用不同的初始值做计算。
随着硬件设备计算算力不断攀升,研究破解攻击SHA系列算法的方法和可能性也越来越高,于是美国国家标准技术研究所(NIST)在2005年、2006年分别举行了两届密码Hash研讨会;同时于2007年正式宣布在全球范围内征集新的下一代密码Hash算法,举行SHA-3竞赛。新的Hash算法将被称为SHA-3,并且作为新的安全Hash标准,增强现有的FIPS 180-2标准。2010年12月9日,NIST通过讨论评选出了第二轮胜出的五个算法,此次被选出的五个算法将进入第三轮的评选,也就是最后一轮的评选,这五个算法分别是:BLAKE、Grøstl、JH、Keccak、Skein。2012年10月2日最终宣布Keccak确定为SHA-3的获胜Hash算法。Keccak算法由意法半导体的Guido Bertoni、Joan Daemen(AES算法合作者)和Gilles Van Assche,以及恩智浦半导体的Michaël Peeters联合开发,它与SHA-2在设计上有极大差别,适用于SHA-2的攻击方法将不能作用于Keccak。
Keccak算法采用的海绵结构如图2-2所示,其中M为任意长度的消息,Z为Hash后的输出,f[b]为置换函数,r为比特率,c为容量,且b=r+c,参数c为Hash输出长度的2倍,即c=2n。从图中可以明显看出,海绵结构有两个阶段:absorbing(吸收)阶段和squeezing(压缩)阶段。在absorbing阶段,消息M首先被填充,然后分组,每组有r个比特,同时b个初始状态全部初始化为0。填充规则为在消息M后串联一个数字串100…01,其中0的个数是使得消息M填充后长度为r的最小正整数倍。根据此规则可以发现最小填充的长度为2,最大为r+1。海绵结构的工作原理是先输入我们要计算的串,然后对串进行一个填充,将输入串用一个可逆的填充规则填充并且分块,分块后就进行吸水的阶段,当处理完所有的输入消息结构以后,海绵结构切换到挤压状态,挤压后输出的块数可由用户任意选择。
图2-2 Keccak算法海绵结构图
由于Keccak采用了不同于之前SHA1/2的MD(Merkel Damgard)结构,所以针对MD结构的攻击对Keccak不再有效,因此到目前为止,还没有出现能够对实际运用中的Keccak算法形成威胁的攻击方法。
以太坊沿用了比特币在Hash运算上相同的Keccak算法,生成32个字节256位的摘要信息。以go客户端代码实现分析,使用的是f [1600] 函数,f [b] 中的每一轮包含5个步骤,总共循环24轮。
func keccakF1600(a *[25]uint64) { // Implementation translated from Keccak-inplace.c // in the keccak reference code. var t, bc0, bc1, bc2, bc3, bc4, d0, d1, d2, d3, d4 uint64 for i := 0; i < 24; i += 4 { // Combines the 5 steps in each round into 2 steps. // Unrolls 4 rounds per loop and spreads some steps across rounds. // Round 1 bc0 = a[0] ^ a[5] ^ a[10] ^ a[15] ^ a[20] bc1 = a[1] ^ a[6] ^ a[11] ^ a[16] ^ a[21] bc2 = a[2] ^ a[7] ^ a[12] ^ a[17] ^ a[22] bc3 = a[3] ^ a[8] ^ a[13] ^ a[18] ^ a[23] bc4 = a[4] ^ a[9] ^ a[14] ^ a[19] ^ a[24] d0 = bc4 ^ (bc1<<1 | bc1>>63) d1 = bc0 ^ (bc2<<1 | bc2>>63) d2 = bc1 ^ (bc3<<1 | bc3>>63) d3 = bc2 ^ (bc4<<1 | bc4>>63) d4 = bc3 ^ (bc0<<1 | bc0>>63) bc0 = a[0] ^ d0 t = a[6] ^ d1 bc1 = t<<44 | t>>(64-44) t = a[12] ^ d2 bc2 = t<<43 | t>>(64-43) t = a[18] ^ d3 bc3 = t<<21 | t>>(64-21) t = a[24] ^ d4 bc4 = t<<14 | t>>(64-14) a[0] = bc0 ^ (bc2 &^ bc1) ^ rc[i] a[6] = bc1 ^ (bc3 &^ bc2) a[12] = bc2 ^ (bc4 &^ bc3) a[18] = bc3 ^ (bc0 &^ bc4) a[24] = bc4 ^ (bc1 &^ bc0) t = a[10] ^ d0 bc2 = t<<3 | t>>(64-3) t = a[16] ^ d1 bc3 = t<<45 | t>>(64-45) t = a[22] ^ d2 bc4 = t<<61 | t>>(64-61) t = a[3] ^ d3 bc0 = t<<28 | t>>(64-28) t = a[9] ^ d4 bc1 = t<<20 | t>>(64-20) a[10] = bc0 ^ (bc2 &^ bc1) a[16] = bc1 ^ (bc3 &^ bc2) a[22] = bc2 ^ (bc4 &^ bc3) a[3] = bc3 ^ (bc0 &^ bc4) a[9] = bc4 ^ (bc1 &^ bc0) t = a[20] ^ d0 bc4 = t<<18 | t>>(64-18) t = a[1] ^ d1 bc0 = t<<1 | t>>(64-1) t = a[7] ^ d2 bc1 = t<<6 | t>>(64-6) t = a[13] ^ d3 bc2 = t<<25 | t>>(64-25) t = a[19] ^ d4 bc3 = t<<8 | t>>(64-8) a[20] = bc0 ^ (bc2 &^ bc1) a[1] = bc1 ^ (bc3 &^ bc2) a[7] = bc2 ^ (bc4 &^ bc3) a[13] = bc3 ^ (bc0 &^ bc4) a[19] = bc4 ^ (bc1 &^ bc0) t = a[5] ^ d0 bc1 = t<<36 | t>>(64-36) t = a[11] ^ d1 bc2 = t<<10 | t>>(64-10) t = a[17] ^ d2 bc3 = t<<15 | t>>(64-15) t = a[23] ^ d3 bc4 = t<<56 | t>>(64-56) t = a[4] ^ d4 bc0 = t<<27 | t>>(64-27) a[5] = bc0 ^ (bc2 &^ bc1) a[11] = bc1 ^ (bc3 &^ bc2) a[17] = bc2 ^ (bc4 &^ bc3) a[23] = bc3 ^ (bc0 &^ bc4) a[4] = bc4 ^ (bc1 &^ bc0) t = a[15] ^ d0 bc3 = t<<41 | t>>(64-41) t = a[21] ^ d1 bc4 = t<<2 | t>>(64-2) t = a[2] ^ d2 bc0 = t<<62 | t>>(64-62) t = a[8] ^ d3 bc1 = t<<55 | t>>(64-55) t = a[14] ^ d4 bc2 = t<<39 | t>>(64-39) a[15] = bc0 ^ (bc2 &^ bc1) a[21] = bc1 ^ (bc3 &^ bc2) a[2] = bc2 ^ (bc4 &^ bc3) a[8] = bc3 ^ (bc0 &^ bc4) a[14] = bc4 ^ (bc1 &^ bc0) // Round 2 …
为了实现高性能,在arm处理器上Keccak完全使用汇编实现,使用go语言封装。
以太坊go客户端,在crypto加密包中,对外封装了使用接口,用来生成Hash值。
// Keccak256 calculates and returns the Keccak256 Hash of the input data. func Keccak256(data ...[]byte) []byte { d := sha3.NewKeccak256() for _, b := range data { d.Write(b) } return d.Sum(nil) } // Keccak256Hash calculates and returns the Keccak256 Hash of the input data, // converting it to an internal Hash data structure. func Keccak256Hash(data ...[]byte) (h common.Hash) { d := sha3.NewKeccak256() for _, b := range data { d.Write(b) } d.Sum(h[:0]) return h }
在应用中,只需要调用crypto.Keccak256Hash或者crypto.Keccak256Hash := crypto. Keccak256Hash([]byte(“hello world”))即可获取指定输入的Hash输出。
在以太坊中有大量的信息以Hash摘要的形式呈现,例如账户和合约地址、交易Hash、区块Hash、事件Hash。下面这个代码就是实现交易的Hash,将交易transaction数据进行rlp编码后,再做Keccak256 Hash运算,最后得到32字节的交易Hash值:0x25e18c91465 c6ee0f79e45016c5dd55eb12424c5d91e59eed237039ba4b239be。
// Hash Hashes the RLP encoding of tx. // It uniquely identifies the transaction. func (tx *Transaction) Hash() common.Hash { if Hash := tx.Hash.Load(); Hash ! = nil { return Hash.(common.Hash) } v := rlpHash(tx) tx.Hash.Store(v) return v } func rlpHash(x interface{}) (h common.Hash) { hw := sha3.NewKeccak256() rlp.Encode(hw, x) hw.Sum(h[:0]) return h }
2.1.2 椭圆曲线的加解密
密码学在工程应用时,一般分为两种加解密方式。一种是对称加密,比如DES、AES,这种加密算法是加解密相关方共享一份密钥,一方加密,另外一方解密,很多应用的密码或者关键信息,都是通过AES加密算法运算存储或者传输的,这种加密算法有个比较突出的优点在于运算相对简单,性能损耗小,效率高。另外一种加密方式叫非对称加密,加解密双方共享不同的密钥,当加密方使用私钥加密时,解密方必须使用对应的公钥解密,比较典型的有RSA和椭圆曲线ECC加解密算法。
RSA(由Ron Rivest、Adi Shamir、Leonard Adleman三个发明人姓氏的首字母组成)基于一个十分简单的数论事实:将两个大质数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥,只要密钥长度足够长,破解是非常困难的。RSA算法,公钥用来公开并加密,私钥用来保留解密,且不可互换,更多应用在加密密钥协商、加密证书签名的场景中。我们常见的https协议,就是采用RSA作为前期交换对称密钥的非对称安全加解密算法。
椭圆曲线ECC和签名ECDSA在数字货币和区块链的应用中被普遍采用。ECC(Ellipse Curve Cryptography)与基于大质数因子分解困难性的加密方法不同,依赖的数学原理是求解椭圆曲线离散对数问题的困难性。
一个通用椭圆曲线的表达式,可以描述为:
Ax3+Bx2+Cxy2+Dy3+Ex2+Fxy+Gy2+Hx+Iy+J=0
图2-3 椭圆曲线
参数不同,描述的椭圆曲线特性不同,差异很大。将一个椭圆曲线用二元三阶方程表示:y2= x3+ ax + b,其中a、b为系数,同时满足4a3+27b2≠0.椭圆曲线如图2-3所示。
一个椭圆曲线群是由曲线上的点和无穷远点O组成的集合。这是一个加法群,定义为:对于椭圆曲线上不同的两点P和Q,则有P+Q=R,它表示一条通过P和Q的直线与椭圆曲线相交于一点(-R), -R关于X轴对称的点即为R,如图2-4所示。
图2-4 椭圆曲线加法运算
对于曲线上的任意一点P,有P+(-P)=0,0是无穷远点。如果P点的坐标是(x, y),那么-P点的坐标是(x, -y);对于椭圆曲线上的任意一点P,有P+P=2P=R,相对于在P点做一条切线,切线与曲线相交于一点,然后取椭圆曲线上关于该交点的对称点;特别的,对于点P,如果y=0,那么交点在无穷远点,则有2P=0.
根据这几条运算法则定义了椭圆曲线的加法,面临这样的一个数学难题,对于椭圆曲线上的点P,其中y≠0,也就是纵坐标不等于0,依据前面定义的加法的计算法则,给定一个整数n,很容易求出Q=nP,也就是n个P相加,但是在已知了P和Q的条件下求取n则是一个很难的问题。
椭圆曲线上的加密与解密一般是运用定义在有限域Fp上的一种椭圆曲线,在椭圆曲线上的加密与解密算法,可以简单描述为下面的过程(以以太坊上两个账户Alice和Bob为例)。
1.密钥的问题
通过选取合适的参数a、b、P建立椭圆曲线EP(a, b),并选取椭圆曲线上的一点G作为基点,Bob选整数K作为私钥,通过运算H=KG得到椭圆曲线上的另外一个点作为公钥,然后把EP(a, b), G, H传给Alice。
2.加密过程
对于要加密的信息m, Alice通过编码将其变为椭圆曲线上的一个点M。然后Alice选择一个随机的数r,在椭圆曲线上计算两个点:
C1=M+rH
C2=rG
然后Alice把C1和C2发给Bob,可以看出Alice是用Bob的公钥进行加密的。
3.解密过程
Bob收到消息后就用自己的私钥进行解密:
C1-KC2=M+r∗K∗G-K∗r∗G=M
椭圆曲线相对RSA,具有如下优点:
❑安全性能更高;
❑160位ECC与1024位RSA、DSA有相同的安全强度;
❑处理速度更快;
❑在私钥的处理速度上,ECC远比RSA、DSA快得多;
❑带宽要求更低;
❑存储空间更小;
❑ECC的密钥尺寸和系统参数与RSA、DSA相比要小得多。
在区块链领域中,以太坊沿用了比特币采用的椭圆曲线secp256k1,y2=x3+ax+b满足六元组关系D=(p, a, b, G, n, h):
p = 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F
= 2^256-2^32-2^9-2^8-2^7-2^6-2^4-1
a = 0, b = 7
G= (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8)
n = 0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141
h = 01
secp256k1由于其自身构造的特殊性,经过优化实现,比其他曲线性能上提高大概30%,也可以有效抵御破解攻击。
ECDSA(Elliptic Curve Digital Signature Algorithm)是基于椭圆曲线生成公私钥进行数字签名和验证的算法过程。
下面以以太坊上两个账户Alice和Bob,在以太坊网络中进行ETH转账交易来说明以太坊的交易ECDSA签名和校验的过程。
2.1.3 签名
交易签名过程:
1)生成一个随机数,作为生成Alice的私钥k;
2)K=kG, G是椭圆曲线secp256k1上生成的点,该曲线上所得点K坐标(x, y)经过处理即Alice的公钥,公钥经过Hash截断后得到Alice在以太坊网络中唯一的账户地址。这个过程如图2-5所示。
图2-5 以太坊从私钥到账户地址生成过程
3)Alice向Bob转账ETH 1000wei,生成交易信息:
rawTx = { nonce: web3.toHex(0), gasPrice: web3.toHex(20000000000), gasLimit: web3.toHex(100000), to: '0x687422eEA2cB73B5d3e242bA5456b782919AFc85', value: web3.toHex(1000), data: '0xc0de' };
4)Alice对转账交易tx的rlp编码的Hash进行签名,得到签名结果为(r, s, v)。其中r和s是标准签名的结果,分别为32字节;v=27+(r % 2),可看作签名结果的一种简单校验,作为恢复签名的recoveryID。以太坊交易签名生成过程如图2-6所示。
图2-6 以太坊交易签名生成过程
签名实质上是使用私钥k对交易摘要进行加密的过程。
交易验证过程:
❑交易校验者,例如矿工miner接收到原始交易和Alice发起转账交易的签名;
❑校验签名,并根据签名和交易Hash恢复出Alice的公钥Q;
❑通过公钥Q,进行Hash截断得到Alice的账户地址。
❑通过比对原始交易中的from域是否和Alice的地址相同。
签名验证实质上是使用公钥Q对交易解密的过程。
以太坊go客户端代码,通过cgo实际集成了比特币的secp256k1库,go代码封装了签名和根据签名恢复公钥。
签名:
func Sign(msg []byte, seckey []byte) ([]byte, error) { if len(msg) ! = 32 { return nil, ErrInvalidMsgLen } if len(seckey) ! = 32 { return nil, ErrInvalidKey } seckeydata := (*C.uchar)(unsafe.Pointer(&seckey[0])) if C.secp256k1_ec_seckey_verify(context, seckeydata) ! = 1 { return nil, ErrInvalidKey } var ( msgdata = (*C.uchar)(unsafe.Pointer(&msg[0])) noncefunc = C.secp256k1_nonce_function_rfc6979 sigstruct C.secp256k1_ecdsa_recoverable_signature ) if C.secp256k1_ecdsa_sign_recoverable(context, &sigstruct, msgdata, seckeydata, noncefunc, nil) == 0 { return nil, ErrSignFailed } var ( sig = make([]byte, 65) sigdata = (*C.uchar)(unsafe.Pointer(&sig[0])) recid C.int ) C.secp256k1_ecdsa_recoverable_signature_serialize_compact(context, sigdata, &recid, &sigstruct) sig[64] = byte(recid) // add back recid to get 65 bytes sig return sig, nil }
恢复公钥:
func RecoverPubkey(msg []byte, s ig []byte) ([]byte, error) { if len(msg) ! = 32 { return nil, ErrInvalidMsgLen } if err := checkSignature(sig); err ! = nil { return nil, err } var ( pubkey = make([]byte, 65) sigdata = (*C.uchar)(unsafe.Pointer(&sig[0])) msgdata = (*C.uchar)(unsafe.Pointer(&msg[0])) ) if C.secp256k1_ecdsa_recover_pubkey(context, (*C.uchar)(unsafe. Pointer(&pubkey[0])), sigdata, msgdata) == 0 { return nil, ErrRecoverFailed } return pubkey, nil } func checkSignature(sig []byte) error { if len(sig) ! = 65 { return ErrInvalidSignatureLen } if sig[64] >= 4 { return ErrInvalidRecoveryID } return nil }
公钥到账户地址的转换:
以太坊中的账户地址和比特币不同,转换相对简单。具体来说就是Hash(公钥)的后20位,这里的Hash算法是sha3-256,可以用代码来表示:
crypto.Keccak256(pubKey)[12:] func PubkeyToAddress(p ecdsa.PublicKey) common.Address { pubBytes := FromECDSAPub(&p) return common.BytesToAddress(Keccak256(pubBytes[1:])[12:]) }
2.1.4 Merkle树和验证
Merkle树又叫哈希树,是一种二叉树,由一个根节点、一组中间节点和一组叶节点组成。最下面的叶节点包含存储数据或其哈希值,每个中间节点是它的两个孩子节点内容的哈希值,根节点也是由它的两个子节点内容的哈希值组成,如图2-7所示。
图2-7 Merkle树形结构
Merkle树的特点在于,底层数据的任何变动都会传递到其父亲节点,一直到树根。当叶子节点有数据不同,如果要比较两个集合的数据是否相同,只需要比较树根就可以了。因此Merkle树的典型应用场景包括:
❑快速比较大量数据:当两个默克尔树根相同时,则意味着所代表的数据必然相同。
❑快速定位修改:例如图2-7中,如果L1中数据被修改,会影响到Hash 0-0, Hash 0和Root。因此,沿着Root --> Hash 0--> Hash 0-0,可以快速定位到发生改变的L1。
2.1.5 MPT状态树
Patricia树,如图2-8所示,是一种更节省空间的压缩前缀树。对于基数树的每个节点,如果该节点是唯一的儿子的话,就和父节点合并。
图2-8 Patricia树
MPT(Merkle Patricia Tree)见名知意,就是这两者混合后的产物,在以太坊(Ethereum)中,它使用了一种特殊的十六进制前缀(hex-prefix, HP)编码,用来对key进行编码。所以在字母表中就有16个字符。每个节点可能有16个孩子,这其中的一个字符为一个nibble(半个字节,4位)。
一个nibble被加到key前(图2-9中的prefix),对终止符的状态和奇偶性进行编码。最低位表示奇偶性,第二低位编码终止符状态。如果key是偶数长度,那么加上另外一个nibble,值为0来保持整体的偶特性。
图2-9 以太坊状态MPT树
MPT树中的节点包括空节点、叶子节点、扩展节点和分支节点:
❑空节点,简单表示空,在代码中是一个空串。
❑叶子节点(leaf),表示为 [key, value]的一个键值对,其中key是key的一种特殊十六进制编码,value是value的RLP编码。
❑分支节点(branch),因为MPT树中的key被编码成一种特殊的16进制的表示,再加上最后的value,所以分支节点是一个长度为17的list,前16个元素对应着key中的16个可能的十六进制字符,如果有一个[key, value]键值对在这个分支节点终止,最后一个元素代表一个值,即分支节点既可以是路径的终止也可以是路径的中间节点。
❑扩展节点(extension),也是 [key, value]的一个键值对,但是这里的value是其他节点的Hash值,这个Hash可以被用来查询数据库中的节点。也就是说通过Hash链接到其他节点。
如图2-9所示,总共有2个扩展节点,2个分支节点,4个叶子节点。
其中叶子节点的键值情况如图2-10所示。
图2-10 叶子节点键值对
节点的前缀,如图2-11所示。
图2-11 前缀定义节点类型
简单来说,MPT树的特点如下:
❑叶子节点和分支节点可以保存value,扩展节点保存key;
❑没有公共的key就成为2个叶子节点;
❑有公共的key则根据公共前缀提取为一个扩展节点;这个扩展节点的key为所有数据在公共前缀之外拼接字节序列,value为子节点的Hash;
❑如果有节点的key和公共的key一致,则该数据保存到下一级的分支节点中。
在以太坊中,MPT数据和验证被广泛应用。在区块链中,区块block打包了大量的交易和合约及账号的状态,如何保证每个区块的这些交易是可以被验证以及状态被频繁的改变呢?实际在区块结构中包含区块头header, header里面包含3种类型的树根,如图2-12所示。
// 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"`
图2-12 以太坊区块头结构
1.状态树:stateRoot
状态树是全局的树:
❑path = sha3(ethereumAddress):以太坊账户地址。
❑value = rlp([nonce, balance, storageRoot, codeHash]):交易次数、账户余额、存储树、合约代码Hash。
其中storageRoot是另一个trie树,存储合约的所有数据,每个账户都有一个独立的storageRoot树。
2.交易树:transactionsRoot
每个block都有一个交易树。
❑path = rlp(transactionIndex):该交易在block中的索引,顺序由矿工决定。
❑value = 交易记录。
该树生成后永远不会被修改。
3.收据树:receiptsRoot
每个block都有一个收据树。
❑path = rlp(receiptIndex):该交易在block中的生成receipt的索引,顺序由矿工决定。
❑value = receipt记录。
该树生成后永远不会被修改。