论文作者:
Ilya Sergey1and Aquinas Hobor2
1 University College London, United Kingdomi.sergey@ucl.ac.uk
2 Yale-NUS College and School of Computing, National University of Singaporehobor@comp.nus.edu.sg
翻译:渡鸦
「让国内外的区块链技术没有时差」。
![](https://i-blog.csdnimg.cn/blog_migrate/23a6e7b28f9c352ab1aa02831c01e0cb.png)
写在【新栏目】前面:
翻译论文无比痛苦。
但是我们的目标是「让国内外的区块链技术没有时差」。
带给大家更多区块链技术干货。
能力有限,还请各路大神多多监督、指教。
欢迎投稿、中英文均可。
摘要:
在本文中,我们探讨了诸如Ethereum等智能合约的多事务行为与共享内存并发的经典问题之间的显着相似之处。我们从Ethereum区块链中检查两个真实世界的例子,并分析它们是如何容易受到与传统并发程序中经常出现的错误的影响。然后,我们详细阐述可观察的合约行为与已深入研究的并发主题之间的关系,如原子性,干扰,同步和资源所有权。描述的并发对象类似的类比可以更深入地了解智能合约的潜在威胁,指导实践,并使用现有的最先进的形式验证技术。
1、介绍:
智能合约是存储在区块链上,拜占庭容错数据库的程序。智能合约可以通过区块链交易触发,并在其块上读取和写入数据[38] 。虽然智能合约以分布式方式运行和验证,但是尽管存在许多复杂的交互模式,包括例如重入和递归调用,但它们的语义表明可以将它们视为顺序程序。这种心理模型简化了关于合约的正式和非正式推理,使得可以立即重用现有的通用框架来进行程序验证[5,16,31,32] ,可用于验证例如写入的智能合约。固化[15] 只有微小的调整。
虽然区块上的所有计算都是确定的[1],但由于交易本身之间的竞争(如为某个给定的区块选择了哪些交易),所以仍然会发生一些非确定性的计算。我们将展示,非决定论可以被敌对方利用,并对合约行为进行推理,特别微妙,让人想起传统并行编程中涉及的已知挑战。
在本文中,我们概述了智能合约的并发执行属性。这种执行可以跨越多个区块链交易(在同一个块内或多个块中),从而违反了仅使用合约的实施和本地状态而不能指定的所需安全属性,正是现有验证方法所关注的[5, 32] 。为了便于共同编程的重用,我们提出以下类比:
在区块链中使用智能合约的帐户就像在共享内存中使用并发对象的线程。
在共享内存中使用并发对象的线程。通过并发对象,我们意味着用于在同时运行的多个线程(进程)之间交换数据和管理交互的大量数据结构[20] 。并发对象的典型示例是区块,队列和原子计数器 - 通常通过诸如java,util,concurrent的数据库使用。同时,在运行时,这些并发对象被分配到正在运行的线程可访问的共享内存块中。由线程同时访问对象而产生的行为 - 即干扰是难以预测的,因此极难理解。
其不利用适当同步的并发对象(例如,具有锁定或障碍物)可以在干扰下会出现数据竞争[2]行为,导致内存完整性的丧失。即使对于无竞争对象,在一个或多个客户端的观点下,观察到的干扰行为可能是错误的。例如,特定的线程可能不会“预见”具有共享对象的其他线程采取的动作,因此可能不会期望该对象以干扰方式改变。
帐户使用智能合约块。智能合约类似于并发对象。他们储存在块状物中而不是一个共享的记忆中; 不是由线程使用,而是由帐户(用户或其他合约)调用。像并发对象一样,它们具有内部可变状态,管理资源(例如资金),并且可以在块内和多个块中的多方访问。与传统并发对象不同,由于计算的事务模型,智能合约的方法而是原子的。也就是说,合约的单一调用(或一系列调用一系列相互调用的合约)是有序执行 - 没有中断 - 并且在成功更新区块链之后终止或者中止并且回到之前的配置。
然而,“原子性自由”的概念是欺骗性的,因为在区块链的水平上仍然可以观察到并发行为:
- 在交易执行时,包含在交易中的交易的顺序并不确定,因此,结果可以在很大程度上取决于其他交易的排序[27].
- 几个编程任务需要将合约逻辑分散在几个区块链交易中(例如,当合约与块之外的世界进行“通信”时),从而实现真正的并发行为。
- 调用其他合约可以被认为是一种多任务合作。通过协同多任务,多个线程可以运行,但不要中断,除非它们明确地“产生”。也就是说,从合约A到合约B的呼叫可以被认为是从合约A的角度来看的收益,合约B在返回时收益。智能合约的关键在于,合约B可以运行合约A的设计者无意识到的代码,这使得情况比典型的顺序设备更接近并发设置。[3]特别地,合约B可以在调用期间修改合约A可能承担的状态。这是DAO错误[9],的精髓,合约B在返回[27]之前调用合约A来修改A的本地状态。然而,重入并不是表现出来的唯一错误,因为:
- 不难想象某种合约被用作其他区块(用户和合约),管理对共享资源的访问以及在某种意义上作为并发库的场景。随着多重交易变得越来越普遍,各种观察到的干涉模式,也应该考虑在内。
我们的目标和动机。幸运的是,在过去三十年中进行的并行和分布式编程的研究为大量的理论和应用框架提供了代码,指定,理由和正式验证并发对象及其实现的框架。因此,本文的目标是双重的。首先,我们将简要概述在智能合约中可能发生的一些已知的并发问题,在更传统的并发抽象方面表征问题。第二,我们的目标是建立一个直观的“良好”和“不良”的合约行为,可以相应地识别和验证/检测,使用现有的正式方法推理并发。
2.并发行为示例
在这里,我们讨论了已经部署在Ethereum块上的两个合约,每个合并都说明了并发类型行为的不同方面。BlockKing的合约,像今天的Ethereum集团许多其他人一样,实现了一个简单的赌博游戏[2]。虽然BlockKing使用不广泛,但我们研究它是因为它展示了Oraclize服务的潜在用途[4],这是一种允许合约与块之外的世界进行通信的服务,从而了解真正的并发性。由于Oraclize服务的早期采用者将其作为该服务的演示,并将其源代码免费提供,希望使用Oraclize的许多其他合约可能会在其实现中反映出来。
我们讨论的第二个例子是DAO中广泛研究的错误[1]。 DAO与18,000多名投资者建立了一个业主管理的风险投资基金;它吸引了当时存在的14%的以太币。随后的攻击使投资者花费了大约360万Ether,当时价值约5000万美元。 DAO采用了我们所说的“不协调的多任务”,因为当DAO向收件人发送钱时,那个接收者能够运行代码,通过DAO的合约状态来干扰DAO的合约状态,假设DAO在调用期间不会改变。
2.1BlockKing合约
BlockKing的赌博如下。在任何时候,都有一个指定的“块王”(最初是合约的编者)。当发送方将货币发送到合约时,在1到9之间生成随机数j。如果当前块号10等于j,则s成为新的块王。之后,BlockKing在合约中收到了一定比例的资金(根据不同的参数,从50%到90%),合约的编者将收到余额。
在确定性系统中,生成优质随机数是很困难,特别是在所有数据被公开存储的情况下,而且对于攻击者来说,都有经济激励。因此,BlockKing利用信任方WolframAlpha的服务,使用Oraclize服务生成其随机数。假设Oraclize是很好的行为,这个随机数选择的策略应该是攻击者的预测。
BlockKing的代码有365行,但是在图1中给出的代码十分有趣; 这里的行号是由Etherscan[2]给出的合约的实际源代码。当货币发送到合约时,输入函数被调用。它设置一些合约变量(行 299-301)),然后发送查询到Oraclize服务(行 303)。
![](https://i-blog.csdnimg.cn/blog_migrate/13ea50e4b107eb4d1ffacf25f980d794.jpeg)
图1.BlockKing代码片段[2]
oraclize_query函数引发一个事件在“真实世界”中可见,然后返回到其调用者,然后退出(行 304)。在现实世界中,Oraclize服务器监视事件日志,服务请求(在这种情况下通过联系WolframAlpha Web服务),然后在指定的回调点(BlockKing中的行 306 )对原始合约进行新的调用。在事件和其回调之间,可能会发生许多事情,在这种意义上,块调用可以在调用oraclize_query之前推出几个块,并在回调时恢复控制。在此期间,块状态,甚至BlockKing合约本身的状态可能会发生巨大变化。换句话说,这是块上的真正并发行为。
什么可以出错?假设多个赌徒希望在短时间内尝试运气(甚至在同一个区块内)。合约没有尝试追踪这种行为。因此,每个新的参赛者都将覆盖第s 299-301行中的上一个数据(关键warrior块和warrior变量)。当回调最终发生时,批次中的最后一名参赛者将享有多次机会,以赢得该批次中参加其他回调的参赛者的宝座。罪魁祸首是来自process_payment函数的第 339-347 行,称为行309中的回调函数的最后一行。
每次process_支付函数被称为warrior区块的最低有效数字被计算并存储到变量singleDigitBlock中.[4]。每次调用process_支付函数时,他都有一个新机会匹配第339 行中的随机数。如果数字匹配,则最后的参赛者在第345行加冠。
2.2DAO合约
DAO的源代码有1,239行,比BlockKing更复杂[[23]由于这个bug已经写得很多了(例如[9,27])),所以我们仅在图2中给出了关键行。问题是第1012行的顺序,(通过一系列进一步的函数调用)将Ether发送到msg.sender,在1014 行,将零余数的msg.sender的帐户为零。
![](https://i-blog.csdnimg.cn/blog_migrate/c4be15de4e155a7d106f4af98f630a57.jpeg)
图.2. DAO代买片段[23]
在顺序程序中,重新排序两个独立的操作对程序的最终行为没有影响。然而,在并发程序中,顺序无害的重新排序的效果可以具有显着的效果,因为操作发生的顺序可以影响线程如何干扰。在DAO中,在第1012 行中发送Ether,在某些多任务的意义上,产生“位于msg.sender的任意(潜在的恶意)合约的控制。
不幸的是,DAO内部状态仍然表明该账户由于其账户余额在第1014 行尚未清零而得到资助。因此,恶意msg.sender可以通过回调DAO合约来启动第二次撤回,该协议将在当控制再次到达1012行时,转一次付款。事实上,恶意的msg.sender可以启动第三,第四等撤回,所有这些将导致付款。只有在支付原始余额的许多倍数之后,最终他的账户被清除。
以前对此bug的分析表明,问题是在于递归或意外的重入。在狭义上,这是真的,但在更广泛的意义上,正在发生的是顺序代码在并发环境中运行的意义。
3干预和同步
3.1共享内存并发中的原子更新
图3描述了错误使用的并发对象的示例(以类似Java8的伪代码呈现),该实例应该使用get和set方法实现“原子”计数器。由于使用了同步原语,左边的并发计数器的实现显然是线程安全的(即无数据竞争)[17] 。然而,有问题的是,在右侧的多线程客户端代码中如何使用Counter类的实例。
![](https://i-blog.csdnimg.cn/blog_migrate/3965707e8a66c4fdb7d17789d62201d1.jpeg)
图. 3. 并发计数器(左)及其双线程客户端应用程序(右)
具体来说,有两个线程并行运行并且它们的操作分离,对线程2的body内的incr()的调用可能会发生在例如在a中的赋值与a之间的调用c.set(a + 1))调用thread1。这将使以下assert语句中的条件无效,使得整个程序对于某个执行失败!
出现这个问题是因为在计数器之上的incr()的实现没有提供客户端代码预期的原子性保证。具体来说,右边的代码是假设incr()的语句之间不会有任何干扰的,因此计数器c将被增加1,a和b将在也是一样的。实际上,并发的运行线程2并不总是这样,不仅a和b将不同,所以后来的c.set()调用也将“覆盖”早期的结果。
与提供incr()的原子实现相比,更好的Counter的设计实现可以提供通过显式锁定或通过Java的同步关键字,fetch-and-increment操作[20,§5.6]来实施。然而,给定唯一的两种方法是获取和设置,Counter的实现具有一个原子寄存器的同步属性,该原子寄存器的共识号[20,§5.1](即可以明确地同意get和 set)正好是1.因此,而不依赖于一些额外的同步,不给予某些预先设定的线程的优先级,通过仅使用get和set来实现c的原子增量是根本不可能的。
也许有点令人惊讶的是,尽管图3的Counter的实现本身并没有缺陷,但是它的弱原子性质使得它在无限数量的线程的存在下变得无用,使得实际上不可能做出任何稳定的(即,有弹性的关于并发变化)关于其内部状态的假设。
3.2并发区块链交易中的原子更新
图4的左边部分显示了一个在Solidity[15]中实现的智能合约,其功能和方法让人联想到原子并发计数器的功能和方法。函数get允许一个查询与当前余额相关联的合约,与一些固定的地址id相关联,而集合函数可以通过msg.value从消息中获取新的余额来更新余额,发回旧的数量和因此返回。
![](https://i-blog.csdnimg.cn/blog_migrate/71b64c87d64189f59179f53a9a1b8403.jpeg)
图.4.计数器合约(左)和同步testAndSet方法(右)
既然获取和设置的两个实体将在一些事务的过程中按顺序执行,那么既不需要同步它们,在Solidity也没有任何明确的方法。然而,不难看出,作为最简单的可能存储(例如,对于某些id相关资金)的实现,由多个不同方使用以更新其平衡,计数器合约与其对应的图中的Java对应,看图3。
例如,假设双方不知道彼此试图增加一个数量的计数器存储的某个值。由于合约不能为他们在一个操作中提供一种方法,所以他们必须首先通过获取查询数量,然后尝试通过设置函数来改变它,遵循与图中实施incr相同的模式。实际上,这两个调用都可以在单个事务中实现。但是由于天然气需求有限,在执行过程中不宜调用多个外部合约。此外,获取的调用可以由块的外部的客户端执行,这意味着获取和设置的连续调用将最终在两个不同的事务中。如果是这种情况,这些调用可能会干扰多方尝试同时修改Counter的其他事务。我们将面对一个常见的问题:不能从本地观察中预测调用函数集的结果。
在共享存储器和块状情况下,问题的原因是缺乏强大的同步原语,允许在同时执行的情况下同时观察和操纵计数器。该问题的一个解决方案是增加计数器的原子可能性,使用testAndSet函数增强计数器(图4的右侧部分)。该功能实现了类似于比较和交换原语[20,第5.8节](在Intelx86和Itanium架构上称为CMPXCHG)的检查/更新逻辑,作为实现多线程之间同步的一种方式。已知testAndSet(和其他一些类似的Read-Modify-Write原语)的一致数量为∞,因此它足够强大,允许任意数量的并发方与操作的结果一致。
关于正式推理和验证的注意事项。运行时并发验证的方法基于探索动态执行跟踪和总结其属性,为检测违反原子性假设和缺乏同步提供了有效的工具[26]。例如,通过将我们的合约转换为相应的共享内存并发对象,可以使用现有工具来总结其跟踪[13],从而可以观察到不需要的交互模式。
原文: https://zhuanlan.zhihu.com/p/29291411