Qtum研究院:如何在Qtum-x86虚拟机上创建智能合约?下篇

2 个月前 · 原创文章

5月19日,Qtum联合创始人、核心开发工程师Jordan Earls发布了《Some Qtum-x86 Tech Details》,该文详细描述Qtum-x86相关技术细节,帮助开发者更加深入了解Qtum-x86虚拟机的运作过程,以及它的实际执行过程。

本篇文章将会分为上下两篇,第一篇用于介绍Qtum-x86合约如何上链及其Qtum-x86组成部分。第二篇讲述如何将DeltaDB设计为共识层上的底层数据存储。此篇为下篇,上篇可戳:Qtum研究院:如何在Qtum-x86虚拟机上创建智能合约?上篇

Qtum-x86消除状态限制

目前,EVM中的状态有很大的限制。它通过键-值对的形式存储,其中每个键和值的大小固定为256位。这种限制可能会加大开发人员管理不同的状态命名空间的难度。因此,Solidity通过自动操作每个存储变量所在的256位键空间中的位置来处理这个问题。这是基于变量名的哈希和/或在源文件中的位置生成的(取决于确切的Solidity版本等,这可以更改)。

通过以太坊节点/钱包将其存储在数据库中的实际方法是通过使用 “state trie”,也就是一颗Patricia树。尽管使用了可验证的加密树,在指定“根哈希”的情况下,节点始终能够证明一段状态的存在性且具有预期的值。但这不能在标准的Merkle树中实现,因为区块链中所有合约的整个状态都会被存储,并且重新计算一个包含数十万个不同状态元素的简单Merkle树,计算量太大,即使验证过程比根哈希的生成更快。

在Qtum-x86的设计中,最大的目标之一就是消除EVM中的状态限制。因此,可以在Qtum-x86使用动态长度的键和值,这就意味着需要一种全新的设计。

因此在Qtum-x86中,我们引入了DeltaDB设计。它是一个基于差异进行操作的新型数据库,不需要将区块链的整个状态编码改为单个可访问的数据结构。对于状态的键-值的大小没有直接的限制,但过大的大小可能会导致更新操作所需的gas成本会过于高昂,因此将很少更新的大数据与频繁更新的小数据划分开来,这样的状态分离操作是有益的。

目前Qtum-x86中的所有状态管理都是手动进行的,没有类似于Solidity对键名称的自动状态管理。未来可能会实现这种功能,但只适用于比C语言更高级的语言。不管怎样,使用传统的键-值数据库中的典型方法来管理键空间是很简单的。例如,如果一个名为“balances”的键-值对映射结构按地址索引并存储一个简单的64位整数,那么它可以像这样存储:

"balances_QddCMpVUf4gKTLseP5XFuVco6xy1YajbK7" -> 1000

当然,“balances_”是一个整数前缀,且地址将存储为原始字节(更好的是使用通用地址字节码格式),从而节省空间。因此,与具有256位长键和值的EVM相比,同等情况下x86的命名空间和键更易于手动管理。这种状态存储在节点/钱包数据库中的实现方式非常地简单直接。

例如:

"state_XZkyE7XAweUtrizgtry1RoMSv1p4zk9Rw8_balances_QddCMpVUf4gKTLseP5XFuVco6xy1YajbK7" -> 1000

其中“XZkyE7XAweUtrizgtry1RoMSv1p4zk9Rw8”为合约地址。当然,这些都是以字节编码的形式存储的,而不是浪费空间的字符串形式,这么写只是为了方便说明。这种方式使数据库的缓存与预测力会比在EVM中的同等情况下更加有效。因为读取由“随机”哈希索引的数据的不可预测性,EVM中有一个众所周知的问题就是要求全节点使用SSD。Qtum-x86节点会保留一些历史数据的副本以保持共识,但也需要对这部分数据进行修剪,因此,如果节点操作人只对当前状态感兴趣,而对历史状态不感兴趣的话,就可以安全删除500个区块之后的大部分历史数据。历史状态对于审计和某些特定的轻钱包应用程序而言特别有用,但对大多数用户来说并不是必需的。

实际在链上验证这种状态数据的方法也比以太坊EVM所使用的方法简单。在比特币的传统轻钱包“SPV”技术中,一个Merkle树的根节点哈希会存储在区块中,这可以用来证明交易确实包含在一个特定的区块中。然而,对于智能合约状态来说,我们不仅要证明简单的合约交易的存在性,也要证明当前的状态,以及交易收据和日志。

为此,Qtum-x86采用了一个“交错”的Merkle树,其中嵌套了每个账户的Merkle树。大多数Merkle树编码单一类型的数据列表。使用交错的方法,Merkle树代替编码成对数据,第一个数据是合约地址,第二个数据是账户“delta tree”。用于根Merkle树的交错方法可以更轻松地定位感兴趣的账户。也就是说,如果一个轻节点始终对某个合约状态感兴趣的话,那编码账户被修改的证明就会很简单。

当前区块中未修改过的账户将不会出现在根树中。更重要的是,这允许某种形式的抗审计力,其中可以反转概念以构建一个账户没有被修改过的证据。这可以通过获取所有已修改账户的列表及其各自的delta树来实现。很容易发现不是所有的数据都被接收(因为Merkle树哈希与区块数据不匹配),或者发现其中的一些数据被修改。

Delta树本身会编码一个“delta”列表。一个delta表示对合约账户的一次更改。这可以包括简单的(不定期)合约执行、合约发起的事件、状态更改或余额更改等。以太坊的“交易收据”概念也被编码成一个delta,这样就很容易证明合约已经执行,以及合约在执行过程中是否出现错误。

就技术而言,DeltaDB的概念比以太坊的state trie概念简单,但却能带来很多好处:

易于实现和审计(例如Qtum)

允许动态长度的键和值

非常易于扩展,允许将来在共识-关键树中对附加的数据进行编码,而不会破坏客户端和程序支持(需要执行硬分叉)

由于非随机索引键,更容易扩展和处理磁盘负载

允许任何账户的SPV节点进行审查检测

允许(缓慢的)检索任意和所有合约的所有历史合约状态,而不会重放交易,并提供数据丢失的证据

当然,它也有缺点:

未经验证的新设计

作为SPV节点获取所感兴趣的合约的所有状态可能需要与全节点进行更多的数据传输,因为不受信任的设置通常不能排除所有历史数据。但是,根据具体的合约设置,应该可以安全地排除一些历史数据。在使用抗审查性时,往返请求的数量也明显更高,因此预计这不会被用作“正常”的通信模式

由于需要空间、中间和临时(500个区块)历史数据,预计与以太坊上的同等设置相比,节点上的磁盘使用量将会增加,否则计算一次以上成本会非常高

智能合约升级

智能合约升级一直是合约生态系统中的一个痛点,EVM在这个方面没有任何优势。EVM不允许直接在已建立的合约账户中更改代码。社区所使用的解决方法是使用代理合约的概念。这种方法使用了一个特殊的“代理”合约,它可以响应某些请求(例如执行升级的请求),但是对于其他功能,它会委托给实际支持的合约,该合约可以通过升级功能来指向另一个新的合约。代理合约还包含支持智能合约的所有相关状态。这样就可以升级合约代码而无需重新部署或修改合约所使用的数据。

当然,EVM智能合约通常是以Solidity语言编写的。这进一步加大了升级过程的复杂度。正如上面的状态数据库所讨论的,Solidity会自动地操作所有状态变量和数组基于EVM存储的单个256位命名空间中的位置。在旧版本的Solidity中,这会引起简单的重构,例如旧状态变量上放置一个新的状态变量会破坏代码结构,从而导致Solidity尝试为旧状态变量读取/写入的不同位置,当然会带来灾难性的结果,也有许多的解决办法。但总的来说,就是由重构/升级的难度,额外的Gas成本,以及初始实施的难度所构成的权衡三角形。因此也有句流传至今的话叫做:“pick two that you want to be optimal”

Qtum-x86想要最大程度地消除这三角形带来的阻碍,并通过以下4种核心方式加以实现:

简单的手动操作和明确的状态管理可避免智能合约开发人员陷入汇编或其他负担中

可以直接升级合约代码,消除了对代理合约的需求

引入了新的智能合约联盟机制,允许访问状态的中央“登记处”,而不会带来过高的gas成本

通过引入一个权限系统,可以使用不受信任或半受信任的合约来安全地执行外部代码委托(委托调用),以确定委托调用的合约可以代表调用合约执行哪些操作

要做的有很多,但首先是状态管理。我们在上面关于状态数据库的部分中提到了这一点,最重要是开发人员可以控制状态所处的位置。尽管256位空间足够可以存储任意数量的不同状态的映射/数组,但它带来了不必要的复杂性,特别是它需要对映射结构中用于索引的键进行哈希处理。要在Solidity中显式执行此操作,还需要将其放到汇编级别,通常要编写许多包装函数。

常规状态变量leu中使用的这些包装函数很难被Solidity编译器优化,并且开发人员需要完全靠自己去处理位置的唯一性、边界检查、字段打包、字段拆分等。在Qtum-x86中对状态的处理就像典型的键-值数据库的操作一样简单。

访问中唯一真正的区别是,没有允许合约对键执行“通配符”搜索的方法,例如“返回数据库中以X开头的所有键”。执行这种查询的成本非常高,很难防止会导致消耗过多gas攻击,因此可以在Qtum-x86的数据库使用这个概念。简单的键名称空间是使用前缀创建的,键可以在不需要哈希计算的情况下存储,并且不需要分解结构或手动打包字节以满足某个常量大小。

接下来就是允许合约代码的升级。出于可证明性的原因,某些智能合约可能会选择禁用这个功能,从安全角度看,以太坊这样的区块链也不能这样升级。但这消除对代理合约的需求,并允许更简单地实现升级机制。

在Qtum-x86中,可以直接从外部合约读取状态。不需要在类ERC20的合约中实现getTokenBalance()等状态访问器函数。相反,可以使用类似这样的函数:

externalState(erc20Address, "balance", addressOfInterest, &balance)

这允许任何外部合约从公共空间读取状态。由于涉及到权限,写入状态的操作会更复杂。但基本上,状态合约会通过使用可信库提供的权限功能,或通过使用显式的修饰符函数来控制谁有权限写入该状态,其中可能带有验证格式等,以及尝试修改的一方已获得授权。可信库系统将允许使用类似代理的系统进行模块化的合约设计。但不是将所有代码委托给单个“代码”合约,而是可以将特定功能委托给特定合约。

此外,引入的权限系统将确保即使在这些委托功能合约中发现了漏洞受到限制。这意味着,如果一个类ERC20的合约委托了像“getBalance”这样的简单功能,则不允许修改状态或者在合约外部发送QTUM。

Qtum

价值传输协议及去中心化应用平台