惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
云风的 BLOG
云风的 BLOG
大猫的无限游戏
大猫的无限游戏
M
MIT News - Artificial intelligence
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Announcements
Recent Announcements
IT之家
IT之家
Google DeepMind News
Google DeepMind News
罗磊的独立博客
爱范儿
爱范儿
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
U
Unit 42
MongoDB | Blog
MongoDB | Blog
S
SegmentFault 最新的问题
B
Blog
博客园 - 叶小钗
月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
C
Check Point Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

smallyu的博客

为什么买的加密货币一定要去中心化 RISC-V 虚拟机无法代替 EVM 虚拟机 Polkadot 的 Existential Deposit 机制错在哪里 利息,套利,交易策略,金融市场 LayerZero 从来不挑战比特币的地位 默认参数引起的以太坊节点运行错误 硬件钱包与资产安全 预测市场比币圈更赌场 为什么去中心化的跨链桥不可能实现 我对于 AI 时代的答案 不要投资任何隐私币 区块链技术世界的三大真理 以太坊 AA 钱包的致命问题是什么 基于 AI 语义执行的 MCP 区块链的设计 一个集成 Geth 和 CometBFT 的兼容层 我的加密货币定投策略(二) DeFi 基础: 理解 AMM 定价机制 Go 语言 GMP 调度器的原理是什么 Web3 项目分析计划 对 Psyche Network 项目的分析 continuation 教程: 理解 CPS 如何开发一个比特币符文(Runes)协议 比特币脚本开发教程 我从王垠的计算机科学视频班学到了什么 区块链技术面试题(2025年版) Rust 语言容易让新手困惑的一个“过度优化” Solana 智能合约开发教程 (1) 尝试开发一个最小 EVM 虚拟机 基于 ZK 的链上身份系统设计 一个 Web3 打赏系统的设计
VRF + BFT 共识引起交易失败的问题
2022-09-02 · via smallyu的博客

2022-09-03

昨天遇到一个问题,是 Ontology 的节点不出块了。节点使用的是 VBFT 共识,网络互通没有隔离,也就没有分叉,报的错是提案(proposal)过来的块,和预期的块哈希(MerkleRoot)不一样。由于种种原因,昨天的问题没有深入去查,用比较暴力的方法先让网络恢复正常。不过提到 VBFT,我想到了以前公司的一些事情。

我是在之前的公司开始接触区块链的,当时的项目号称自研区块链,也是用 VBFT(VRF + BFT)的共识,不过在共识方面不那么说,叫成 UBFT 还是什么。

我猜测 VRF + BFT 的主意是 Ontology 提出来的,我之前的公司把这种创意抄了过来,模仿着实现了一下。说来讽刺。

主要想说的是之前公司由于对 VRF + BFT 不靠谱的实现,引起的一个隐藏的 bug。那件事情距今快 2 年了,不记得当时为什么没有写博客记录一下,可能是在忙着做 PPT?昨天遇到共识相关的问题,我想起有那么一回事,正好现在有时间有心情写一下。由于过去时间太长了,细节上可能有出入。

背景

先介绍一下那个项目的情况,主打的特点有几个。

一个是异构多链,含义是可以在同一个节点上包含多条异构的链。异构是指一条链可以使用不同的共识机制、基于不同数据库运行起来。多链是指多条链可以在同一个节点上运行,因为觉得一条链不够用,一条链就相当于传统业务里的一张数据表,多条链可以方便地进行数据拆分,在联盟链的场景下更好地支持业务。异构多链,可以理解为把以太坊不同 chain id 的链,用同一个二进制包启动了。现在的某条开源联盟链,还在拿从异构多链演变来的灵活装配作为一大特点呢,猜猜为什么。

再一个是多数据库的支持,同时支持很多关系型数据库和非关系型数据库,做法是针对各种数据库,写数据操作的中间层做适配。

还有就是共识机制方面,基于开源的 Tendermint 项目。Tendermint core 是一个对 BFT 类共识的实现,在那个基础上,做的改动是把轮询选择提案节点,用 VRF 函数,替换为随机选择提案节点。另外还增加了对分层共识的支持,也就是共识组的概念,每隔多少个块换一次共识组,换共识组的方式借鉴 BFT 的流程,保证换共识组过程的安全性。分层共识这个理念也不知道起源于哪儿,可能同时期的项目流行这个?

项目的这些特点都是在我接触之前就已经开发完成的,我也只是有所了解。

我当时遇到的问题是,如果向区块链发送一笔失败交易,节点会立即返回交易失败的结果,然后如果再次发送一笔失败交易,第二笔交易的结果将迟迟不返回,节点不出块了。接下来如果仍然是失败交易,第三笔、第四笔,都会是同样的现象。这个时候,如果发送一笔正确的交易,节点会立即返回结果,之后一切恢复正常。而且这种现象是概率性出现的,并不是每一次失败交易都会引起问题。

前提

首先是失败交易指合约返回执行结果为失败的交易。区块链系统的交易失败有两种,一种是交易不能被执行,另一种是交易能被执行,但是合约中返回了合约层面的失败。那个项目并没有严格区分这两种失败的类型,合约有权限返回交易层面的失败,这其实是有问题的设计。

不正确的交易,将会在提案的时候,被忽略掉,因为不正确交易没有必要记录在区块链上。加上项目对失败交易错误的处理,造成的现象就是,合约执行失败的交易,会被忽略掉。这是前提。

BFT 共识的基础,是投两轮票,最终确定一个块。不管是什么 BFT,在前面加什么字母,不管通过多么复杂的流程决定出哪个节点提案、如何提案,不管对共识的效率做什么优化,是并行提案还是流水线共识什么的,只要是 BFT 类的共识,都是投票两轮。对两个阶段的命名可能不一样,不管是用 proposal 还是 prepare 来描述,都是那样一个过程。

BFT 的流程,是先有一个节点生成一个块,然后把这个块发送给其他节点,如果超过 2/3 节点同意,会进行下一轮投票。第二轮投票如果超过 2/3 节点同意,这个块就算确认下来了。两轮投票是理解 BFT 共识的关键。至于为什么投票两轮就可以达到 3f + 1 的容错效果、为什么至少要两轮,我也不知道。

忽略掉失败交易的操作,是在检查交易的过程中完成的。项目里有两次检查,共识前检查和共识后检查。有一些交易是没办法在共识前进行检查的,比如在合约里进行的写数据库操作,如果共识前就写库了,然后共识失败了,数据不就乱套了吗。所以只能共识后进行检查。这是第二个前提。

第一笔失败交易

我们根据 bug 的现象分析一下,第一笔失败交易的流程是正常的。一笔失败交易进来,在共识之前是不会检查出失败的,所以预提案的节点正常提出了一个块,分发给其他节点,进行第一轮投票。之后正常进行第二轮投票,在确认块的阶段,写入块之前,会进行共识之后的检查,检查过程中发现交易失败,并且这个块只包含这一笔交易,这个块就作废了,没出块。同时,其他节点也都会返回消息告诉提案节点,这个块没出来,这笔交易失败了。所以第一笔失败交易是正常返回结果的。

第二笔失败交易进来,按照同样的处理流程,一切都应该是正常的才对。因为即使是失败交易,即使是在确认块的阶段,如果检查失败了,也会广播处理结果给其他节点。整个协议中投票失败或者落块失败,都是用空消息表示。其他节点不会因为交易失败,就收不到消息苦苦等待超时。那么既然 BFT 协议的流程没有问题,为什么还是出现 bug?

这个时候要提到项目在 VRF 方面的改造。

在 BFT 的协议中,是需要一个节点去生成一个块,分发给其他节点开始进行第一轮投票的。那么由哪个节点来进行这个生成块的操作呢?总不能是同一个节点吧,那就太中心化了。Tendermint 的做法是依次进行,比如有 4 个节点,第一次节点 A,第二次节点 B,这样轮询。

VRF(Verfiable Random Function)做的事情,是改变依次选择节点的方式。因为如果按照顺序来,那很容易预测到下一轮要由哪个节点去生成块,顺序可以预测之后,就存在节点被贿赂、节点被攻击等安全隐患。VRF 的功能是参数相同结果一定相同,参数不同则结果随机。把块高度、投票的轮数作为参数,就可以很好地实现,每一个块都能由随机的节点来生成,无法预测。这个改动也是作为项目的一个亮点的。

不过 VRF 存在一个问题,既然是随机的,那就有一定可能,第一次随机到节点 A ,第二次也随机到节点 A,这样的概率还是不小的。如果节点 A 是恶意节点,然后由节点 A 连续两次生成块,会给网络带来一些负担,虽然不至于破坏网络,但也是一点小小的麻烦。所以项目为了解决这个问题,在 VRF 的基础上加了黑名单的机制。

如果上一轮是节点 A 生成块,就把节点 A 放到黑名单里。如果 VRF 的结果在黑名单里,就再 VRF 一次,避免重复选择相同的节点。

第二笔失败交易

不返回结果

结合 VRF 和黑名单,再来看看第一笔失败交易发生了什么。节点 A 收到交易,会先把这笔交易广播给其他节点,然后打包成块进行投票的流程。此时节点 A 在黑名单里。投票失败后,节点 A 返回失败,并且这笔交易已经不在节点 A 的交易池里了,因为已经处理过了。

那么节点 B 呢?块里面的交易验证失败了,但是交易池里收到的交易还在,因为这笔交易还没有处理啊,处理的只是广播过来的块里面的交易。这个时候是不是应该把交易池里面的交易删掉?对,但是没删。所以造成一个问题,节点 B 被选作生成块的节点,把这笔交易打包了一下,广播了出去。这个块当然也是提案失败的。此时节点 A、节点 B 都在黑名单里。

以此类推,就这一笔交易,一轮下来,4 个节点全在 VRF 的黑名单里。但是对这笔交易结果的返回是没有影响的,因为交易结果在节点 A 的时候就已经返回了。

第二笔失败交易过来了,所有节点全在黑名单里,会发生什么?当然不能选不出节点,节点全在黑名单里,黑名单就失效了。VRF 的结果是哪个节点,就是哪个节点。

分析一下第二笔失败交易。同样是节点 A 收到交易,假如这一次是节点 B 负责生成块,然后这个块验证失败了,节点 B 就会删掉这笔交易,对吧,这个没问题。

注意,删掉交易的同时,通知客户端,交易失败了,返回交易结果。节点 B 被选中,节点 B 生成块,节点 B 返回通知。但提交这笔交易的客户端,连的是节点 A 啊!

第一笔失败交易为什么会收到响应?因为黑名单还没有失效,所有节点都处理了一遍交易,所有节点都返回了一遍交易结果。现在黑名单失效了,只有节点 B 会返回结果,所以节点 A 的客户端收不到交易结果。

那为什么黑名单失效,就不能像第一笔交易一样,所有节点都处理一遍?

阻塞后续交易

接着分析一下第二笔失败交易,节点 C 还有交易啊,节点 C 上面的这笔失败交易还没处理呢,节点 C 就开始用 VRF 选节点了。

刚才提到,此时黑名单失效,VRF 选出哪个就是哪个。刚才选出了节点 B,这一轮有没有可能再选一次节点 B?黑名单失效,就变得可能了。这个时候如果又是节点 B 负责生成块,会发生什么?

节点 B 生成不了块,因为节点 B 已经没有交易了,它已经把唯一的失败交易,在上一轮就删掉了。也就是说,在新一轮的共识过程中,4 个节点全部在等节点 B 生成块,节点 B 自己也知道该自己了,但是节点 B 拿不出块,节点 B 直接放弃这一轮共识,进入下一轮,并且节点 B 没有发出任何消息。

在分布式系统中,没有消息是一件可怕的事情。其他节点都在等节点 B 呢,节点 B 自己玩了。这个时候,节点 B 的轮数要比其他节点快一轮。

在 BFT 共识中,有两个索引值,一个是块高度,一个是共识的轮数。同一个块高度,有可能因为块没确认,就经过很多轮共识。由于节点 B 自己没生成块,轮数增加了,其他节点还不知道。

现在,所有节点都在 VRF 的黑名单里,节点 B 的共识轮数高于其他节点,

如果节点 A 再收到失败交易,有两种情况。一种情况是 VRF 又选中节点 B 了,节点 B 提出的块会被拒绝,因为其他节点还在等节点 B 上上轮的块,它拿出了高轮数的块,是对不上的。另一种情况是,VRF 选中了其他节点,那其他节点首先要等节点 B 上上轮的出块超时。超时之后,其他节点把轮数最高的数值同步一下,共识就算恢复正常了,然后 VRF 再选。

但是注意,这可是一笔失败交易,此时黑名单仍然失效,即使共识恢复正常,也还是有概率重蹈整个覆辙,节点 A 仍然收不到交易结果。至于具体的概率是多少,就懒得算了。

总结

可以看到,这个 bug 是由很多系统性的不合理设计共同造成的,直接原因在于 VRF 黑名单的失效,因为所有节点都在黑名单里了。或者说,黑名单没有及时清空,原先的错误之处在于,只有在块高度变化的时候才清空黑名单,可能是认为每个块的产生都应该由不同的节点来处理。这种想法的失误在于忽略了相同块高度的时候,共识的轮数也会发生变化,每一轮都会产生一个新的块。所以只要在共识轮数发生变化的时候,也清一下黑名单就好了。实际的代码改动只有两行。

上面写的东西,可能我自己也不想仔细去看,不好理解、抽象,而且文字的表达能力也弱,看起来费劲。这种类似状态机状态转换的文字描述,看起来是很痛苦的事情。尤其是内容和当时项目的耦合很深。总的来说,对于这个问题的分析和解决,我认为在逻辑上是自治的,能很好的解释成因和现象,以及用最简单的方式在表面上修复它。

时隔近 2 年的时间,我竟然还能记起来这些,感觉也是很奇怪。