近日,树图区块链Conflux研究院在介绍延迟执行策略时提到树图区块链Conflux的轻节点设计,并为我们点明了疑惑。
树图区块链Conflux提出解释道:为了节约计算资源,在转发区块时可以只检查区块的格式和引用是否合法(包括工作量证明),而尽量少检查区块中所存的状态根(State Root)。但是不强制检查状态根在安全性上会带来一些隐患,例如有些矿工可能会偷懒,只打包交易而不管实际执行交易,也不去验证收到的区块里的状态根的正确性——这样就可以省下执行交易、维护和更新状态的开销。
当然,对于自己亲力亲为执行所有交易的全节点来说,即使有一小撮矿工偷懒实际上也不会影响他们对于当前共识状态的判断。只要偷懒的矿工仍完整地执行共识层协议,按照协议要求检查区块间的引用关系和选择引用的区块,则他们的算力依然为保证账本中的交易顺序不可篡改作出贡献。
但是对于轻节点来说,区块内的状态根不对就是一个相当严重的问题了。这是因为轻节点自己没有能力维护当前整个共识系统里所有账户的状态——否则也就不是“轻节点”了——只能选择信任全节点提供的(带有默克尔树证明的)状态。如果多个全节点提供的状态不一致,轻节点就难以判断当前的真实状态。
以比特币的轻节点为例,只需要保留每个区块的区块头(Block Header)就可以用 SPV(Simplified Payment Verification)的方式验证一笔交易是否已被确认了。
比特币的 SPV 验证步骤如下:1)通过区块头中保存的默克尔树根(Merkle Root)和全节点提供的默克尔树证明(Merkle Proof),可以非常容易地验证某个区块里包含特定的交易;2)如果包含交易的区块后面跟了足够多的区块(比如说6个)就可以认为这笔交易已经被确认了——实际上这里是区块被确认,但因为比特币最长链上的所有交易都会被执行,所以区块被确认等价于交易被确认。
理论上,只要能生成足够多的比特币区块,配合对于网络连接的日食攻击,就可以骗过轻节点,让其以为一笔不在真实的比特币网络最长链上的交易已经被确认了,从而实现双花攻击。现在比特币的 UTXO 已经达到了数 GB 的规模,很多场合只能通过轻节点用 SPV 的方式确认交易,理论上都会受到上述攻击的威胁。
但是事实上很少听说因为 SPV 验证受到这种双花攻击的案例,这又是为什么呢?树图区块链Conflux研究所同样为我们进行了解惑。
主要的原因还是在于成本。以 6 个区块确认为例,欺骗轻节点实现双花攻击需要攻击者至少挖到 7 个比特币区块,并且这些区块都不在比特币的最长链上——否则就变成真的确认交易了。
如果挖的这些区块难度和真实的比特币网络相当,则意味着攻击者要放弃 7 个得到比特币区块奖励的机会。按照现在的比特币每个区块 12.5个比特币的奖励计算,除非双花攻击的交易价值超过 75 个比特币,否则这样的攻击就是亏本的,因此自然不会有人去实施。如果再加上实施日食攻击的成本,则上述攻击就更加无利可图了。
所以在比特币网络中,轻节点只需大致知道正确的挖矿难度值,就不用担心在金额不太大的交易中因为采用 SPV 确认规则而被骗。通过 SPV 方式确认交易,至少在比特币的区块奖励再减半几次之前都还是相当安全的。当然,如果真的是大额比特币交易的话,就需要增加等待的区块数或者用全节点验证了。
以太坊的轻节点也可以通过类似的方式验证交易执行的结果或查询账户的状态,只需全节点根据节点中对应交易收据和账户状态的默克尔树根提供相应的证明即可。
与比特币这样“一根筋”的区块链系统不同,树图区块链Conflux 为了更高的系统性能采用了并发出块的树图结构以及更快的出块速度(平均每秒4块),并且减少了非必要情况下对于状态根的验证。这些改动不会影响全节点对于共识的判断,却可能为轻节点验证当前状态带来困难。
为此,树图区块链Conflux 引入了 Blaming 机制来帮助轻节点快速地确认当前账户状态和每笔交易执行的结果。
简单来说,每个区块自己区块头的 Blaming 域指出自己认可的上一个“正确的主链区块”,这里的“正确”指区块的状态根和 Blaming 域都是正确的。例如如果一个区块认为自己的父区块就是完全正确的,则 Blaming 域就填 0;如果认为父区块不正确,但是祖父区块正确,则填 1;以此类推,如果认为祖父区块也不正确则相应地要填一个大于1的整数用来指示最后一个正确区块的位置,即落在中间的所有区块都会被指为“状态不正确的区块”。
对于状态根的判断比较容易理解:打包新区块的矿工节点需要沿着枢轴链执行每个 Epoch 中的交易,在这个过程中自然会知道每个处在枢轴链上的“主链区块”应该有什么样的状态根,因此就可以判断出实际处在枢轴链上的区块是否填对了这一项。而且因为按照枢轴链指定的顺序执行所有交易本来就是每个全节点都应该做的,所以上述检查并不带来额外的负担。检查过后,就已经可以确定哪些主链区块的状态根是正确的了。但是由于主链上有很多区块,显然不可能每次都一一罗列出哪些区块的状态根是对的,哪些是错的。这就用到了 Blaming 机制的第二部分——关于之前区块 Blaming 域的 Blame。
每个区块 A 在 Blaming 域都会指明它认为的主链上最后一个正确的区块 B,这表示区块 A 认可区块 B 的所有观点:除了认为区块 B 的状态根正确以外,还认可区块 B 的 Blaming 域是正确的。这就意味着区块 A 也认可了区块 B 所指示的在 B 之前的最后一个正确的主链区块 C,从而进一步认可了区块 C 所指的 C 之前最后一个正确区块 D,……直至创世块位置。
因为创世块总是对的,所以上述 Blaming 的过程是有限的,每个区块在 Blaming 域填的数也不会超过该区块的高度。
通过这种方式,就可以把每个区块 Blaming 域存的一个数字扩展到一组状态根得到当前区块认可的主链区块。下图的例子即说明了如何通过 Blaming 机制扩展对于“正确区块”的判断。
由于 Blaming 域是在每个区块的区块头部分的,所以轻节点也可以很容易看到矿工们对于其它区块的状态根是否正确的看法。根据这些信息,轻节点就可以比较容易地判断哪些区块的状态根是值得信任的,哪些区块的状态根有问题了。然后只需选择相信基于值得信任的状态根做出的证明即可。