还记得这张图吗?得到 300BTC 了吗?
在上周,区块链游戏比特币挑战(Bitcoin Challenge)风靡全网。游戏非常简单,就只有一张图,在这张图中藏着 310.61 个比特币,换算成人民币价值 1400 万元,只要你能看出来,这笔巨款就归你了。 不知道聪明的你能否“看出”这笔巨款,反正营长看了半天也没看出个所以然来。如果说这个悬赏太抽象。 现在有一个简单一点的悬赏,而且赏金也不少,目前有 15个 以太币,每天还在以 1 个以太币的速度递增。 密码学朋克、以太坊骨灰级粉丝 Zac Mitton 发布了一个智能合约,只要你能找出其中的漏洞,智能合约会自动将赏金转给你,爱钻研的你一定不能错过这个机会,心动不如行动,快来看看他的悬赏吧。
译者 | Guoxi
该悬赏智能合约账户
一、背景介绍
在以太坊上递归检索动态数组或链接列表可能会造成很严重的安全问题,因为攻击者可能会增加它们的大小以使得智能合约出现异常。
面对这个风险,当前的对策是使用区块燃料限制和“锁定”机制,不过在我看来,最简单的方法是使用堆数据结构(Heap)。
为了验证堆数据结构的安全性,我创建了一个使用堆结构的智能合约,并在其中加入了悬赏函数,如果你可以通过漏洞让智能合约中的堆结构不再满足某一个堆结构的定义,那么智能合约会自动向你发放悬赏。
初始悬赏额为10个以太币,在未来我准备每天增加一个以太币的悬赏,当前赏金已经达到了15个以太币。
心动了么?快来试试吧。
二叉堆(Binary Heap)数据结构是优先队列(Priority Queue。一个使用场景是交易委托账本,即券商对交易所的委托交易信息,它处于证券交易的核心位置,所有交易都围绕它进行)的一个最简单实现。
二叉堆是“部分排序(partially sorts)”的数据结构,所以最高优先级数据(最大值或最小值)就位于根节点。
以太坊允许用户将数据插入智能合约中,这样可能会带来迭代访问花费太多燃料的问题,换句话说,就是带来燃料限制攻击的问题。
如果直接使用数组,攻击者可以将数组填充到正好超过单笔交易中允许燃料上限的临界点(当前区块燃料限制为800万)。如果攻击这样一个智能合约的成本远低于攻击所得时,它将受到攻击。所以说这样做很不安全,写智能合约时一定要注意这个问题。
堆数据结构可以帮助解决这些问题,因为堆结构不需要逐个迭代访问元素,它迭代的次数仅为二叉树的高度(log(n))。
不幸的是,即使在通常情况下许多树结构的时间复杂度都是O(log(n))(对数阶),但在公开可见的以太坊智能合约中使用它们并不安全,因为攻击者可以寻找机会使树结构退化(让树结构的某一个分支变得非常长,这个过程就被称为树结构的退化),从而将时间复杂度提升为O(n)(线性阶)。
平衡二叉树的出现解决了这个问题。平衡二叉树使用算法保证左右两个子树的高度差绝对值不会超过1,也就是说用算法保证它们不会退化。
平衡二叉树在插入数据期间通过旋转或交换节点以保持平衡,从而即使在最坏情况下也能保持其O(log(n))的时间复杂度。
二叉堆及其时间复杂度
时间复杂度 图片来源:维基百科
二叉堆是一种部分排序的平衡二叉树,在最坏情况下也能保持O(log(n))的时间复杂度。
如果你需要使用完全排序(fully sorted)的平衡二叉树,可以使用2-3-4树,红黑树或AVL树。 Piper Merriam用Solidity编写了一个AVL树,通过AVL树他编写了在未来定时执行交易的以太坊闹钟(Ethereum Alarm Clock)。
2-3-4树 图片来源:维基百科
2-3-4树把数据存储在称为元素的独立单元中,由元素组合成节点,每个节点都是下列之一:
红黑树 图片来源:维基百科
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。除了二叉查找树的一般要求以外,红黑树还有如下的额外要求:
AVL树的旋转操作 图片来源:维基百科
堆数据结构可以让你从根结点处快速找到最高优先级的数据。不过,它不能像其他树结构一样快速地将数据从大到小排列。
举例来说,堆数据结构可以帮助实现去中心化交易所中的交易委托账本功能。
当有人提交卖单时,智能合约需要找到出价最高的买单以查看是否相匹配(反之亦然),匹配则促成一笔交易。
如果没有匹配,智能合约不需要找到下一个出价最高的买单,所以说堆数据结构可以胜任这种使用场景。
如果匹配,智能合约调用函数extractMax()取出堆结构中数据的最大值(出价最高的买单)并将其从堆中删除。堆结构会自动重新调整,将新一个最大值(出价最高的买单)放在堆结构顶部的根结点处。
我想来想去,越发觉得堆数据结构可以用来解决以太坊上的诸多问题。请记住,发起交易的成本(燃料费用)仅与在区块链上执行的逻辑有关。在链下,我们可以轻松地遍历所有数据并在适当的时候对其进行本地缓存。
在我给出的代码中,有一个可以实现这个功能的dump()函数。还有一个用JavaScript写的index.js文件可以用于重建堆结构,并以可视化的方式打印堆结构中的数据。
完全排序树结构的唯一好处就是可以实现数据从最大值到最小值的迭代,但这也会带来刚才所说的区块燃料限制攻击问题。很难想象会有哪个需要AVL树或红黑树的应用程序,不会遇到燃料限制攻击问题。
接下来在Truffle智能合约中导入程序库:
在使用前调用程序库中的init()函数。
堆数据结构允许插入,删除和删除最大值等操作。
这个特定的堆结构还支持用于解决竞态条件的函数getById()和extractById()。节点结构体中(struct Nodes)只有id(用于索引的身份)和priority(优先级)属性,这两个属性被打包到1个存储槽(storage slot)中.
不过你也可以用任意的数据拓展这个结构体,只需要创建一个指向结构体的映射,并使用结构体中的id做匹配即可。
你可以把堆结构简单地想象成一个数据存储。在其中可以插入数据,提取数据或查找并删除最大的元素。请记住,不要使用除了API之外的任何方式操作堆结构,否则可能会破坏它的数据完整性。
我创建的这个堆结构是一个最大堆(根节点中的值最大),如果你想把它作为最小堆(根节点中的值最小)使用,只需要给它加上反转符号,也就是乘以-1。
错误的输入将返回默认的零节点:Node(0,0)。在大多数情况下,函数不会抛出任何错误,所以这需要你自己处理错误。如果你想在让函数抛出错误,请在返回的节点上执行:
请注意,如果你想要从公有函数(public function)返回Heap.Node数据类型,你必须在代码中使用选项experimental ABIEncoderV2。
对于以太坊代码而言,保密性和安全性尤为重要,因为以太坊可以说是有史以来最恶劣的编程环境。
在当前的范式转变(paradigm shift,指旧的模式遇到巨大困难而被新模式推翻的过程)中,发布悬赏是团结各方力量推动变革的最好方式。我的赏金将从10个以太币开始,在至少未来一个月内会随着时间的推移不断增多。
欢迎大家前来挑战。不同于传统的悬赏需要你报告漏洞并祈求得到发布方的怜悯获得赏金。我的这笔赏金是锁定在智能合约中的以太币,你一旦报告漏洞就会收到智能合约自动发放的赏金。
实际上,如果你找到了一个潜在的攻击向量(attack vector,指黑客用来攻击计算机或者网络服务器的手段),在成功利用它(确保以太币已经转到你的帐户)之前你最好不要向任何人透漏任何信息。
甚至你可以匿名来夺取赏金,但我希望你想办法记录漏洞被攻击后的详细信息,并开启一个GitHub问题(GitHub issue)帮助大家学习交流。
智能合约地址:0xd01c0bd7f22083cfc25a3b3e31d862befb44deeb
智能合约代码地址:https://github.com/zmitton/eth-heap
首先,我写了Heap.sol程序库。
然后,通过这个程序库我写了第二个智能合约BountyHeap.sol,它给出了这个任何人都可以向其发送交易的“公共”堆数据结构的所有操作。
在第二个智能合约中,我用堆结构的定义新建了一个堆结构,并编写了一个公有函数用来在某些属性被攻击后自动发放赏金。
在我创建的堆数据结构中,所有子节点的值应小于或等于其父节点的值。如果你能够通过漏洞攻击让智能合约进入任何非正常的状态,只需调用如下函数:
智能合约将发放全部的赏金,当前已到达15个以太币,价格超过2万元人民币。
为了保证堆数据结构的安全,你需要保持许多其他细微特性的完整性。正如我在下面所描述的,我已经编写了相应的函数,如果你找到了某个漏洞,它们都会自动发放赏金。
二叉堆是一棵完全二叉树(完全二叉树指,若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边)。这意味着它可以使用动态数组(不使用指针)实现。
即使从任何位置插入或删除节点,这个动态数组都不应包含影响性能的空点(empty spots)。实际上这种架构可以显著降低燃料成本!如果此属性被破坏,则堆数据结构肯定会损坏。
根据我的设计决策,函数的剩余部分中我给每一个节点分配了一个独一无二的id。此id用来让堆结构组织任意类型的数据。例如,如果你想要一个出价最高的买单(buyOrder结构体),通过堆结构的getMax()函数就可以找到它,然后使用返回的id在单独的映射中查找这个买单。
id还允许用户删除某个特定节点并在此处使用其他值(如它的索引),这样可能会由于其他用户之前的交易而发生无法预测的变化。
为了满足这些使用场景,程序中使用了从id到索引(index,在nodes数组中)的映射。无论何时插入、删除或移动节点,都会在后台自动更新。
如果由于你的漏洞攻击使堆数据结构中出现了多个具有相同id的节点,这意味着出现了严重错误,请使用以下命令拿走属于你的赏金:
此外,映射中请不要让某个id指向数组中的空节点或不同节点,反之亦然。所以,在拿取赏金之前请先使用以下代码证明:
由于二叉堆数据结构的简单性,即使在最坏的情况下所有的燃料成本也是以对数形式增加,这比其他方案便宜得多。
而且由于堆结构是完全二叉树,所以它可以使用数组来实现,使得构造堆结构的燃料花费也远低于构造其他数据结构.
因为它不需要指向子节点和父节点(这样做需要最耗燃料的东西:存储空间),而是使用最简单的算术方法索引,即从子节点移动到父节点也就是将索引除以2,从父节点移动到左子节点或右子节点也就是将索引乘以2或索引乘以2再加1。
用下图举例,4号节点的父节点是2号节点,3号节点的左子节点是6号节点,右子节点是7号节点。
基于数组的树结构
为验证使用堆数据结构的燃料成本是否低于区块燃料限制,我做了如下实验。
通常情况下,处理500个数据项的数据集的平均燃料成本是:
根据经验,每次数据项的数量翻倍时,这些函数的燃料成本大约会增加20000.下图是实验测得的堆数据结构插入操作和读取操作消耗的燃料。
插入操作的统计数据
提取操作的统计数据
从图中我们可以看出,操作堆数据结构消耗的燃料远低于当前800万的区块燃料限制,所以说鉴于以太坊目前的架构,使用堆数据结构就不再需要区块燃料限制和“锁定”机制。
堆数据结构是一种非常好的解决方案,如果你有任何异议,欢迎来挑战我。
挑战代码地址:
https://github.com/zmitton/eth-heap
--听说标星看大图更舒服哟!--