Qtum-x86虚拟机存储区实现租赁 限制节点存储的数据总量

2019-05-31 11:00:43
来源: 腾讯

  【摘要】 Qtum

编者的话:本提案为Qtum联合创始人以及核心开发工程师Jordan Earls发布在Github的Qtum x86虚拟机最新进展中(点击阅读原文或复制链接至浏览器打开:https://x86.qtum.org/),他提出了一种关于存储的全新技术提案——Qtum-x86虚拟机中所有存储区都可实现租赁机制,高效的节省了虚拟机内存。

就目前而言,在EVM基础设施不受影响下,全新租赁机制是一种“被动”的方式去实现,不需要特殊的合约逻辑来处理相关的租赁过程,也不需要支付所消耗存储区的租金。该技术设计将有效地限制节点需要存储的数据总量,同时还能限制轻量级SPV节点对智能合约进行有效治理与交互所需的总空间。

为什么会提出EVM租赁机制?

区块链基础设施技术背后的逻辑都面临着一个问题:“区块链虚拟机数据越来越多怎么办?”,因此存储空间的消耗长期以来一直是区块链领域内一个受人关注的话题。任何区块链,尤其是具有智能合约功能和附加状态的区块链,据估计存储空间的需求会在短短10年内超出大多数计算机和服务器的存储能力。

这种磁盘空间需求的膨胀将导致全节点会集中到那些能负担得起价格高昂的具有非常大存储的价格的人手中。简而言之,未来就会出现由于存储设备的门槛导致资源的集中,也就与去中心化背道而驰。

举例而言,数年来随着区块链市场的蓬勃发展,以太坊交易数量越来越多,单个区块体积的最大值限制使得区块空余空间显得越来越小。如图,相比比特币而言以太坊的区块大小更加呈现增量上升的模式,甚至在2017年后以太坊的区块大小由不足0.1TB上升至接近0.4TB。

因此,越来越多的开发者和相关技术都在进行底层基础设施的探索,想要商业应用于区块链技术真正实现技术的融合,就需要不断去提出新的思想和新的技术探索,因此本文提出共享存储的设想,帮助区块链的基础设施早一步更好的搭建商业设施的桥梁。

QIP-17:Qtum-x86内的存储区租赁

对此在Qtum x86设计上所做的变更可以划分为以下3个部分:

DeltaDB 重新传播和租赁行为

“休眠”状态的智能合约行为

“唤醒”休眠状态的方法

首先,用于不同状态的术语解释

· 活跃:该状态要求支付一定的租金,从而在区块链上保持其活跃性并易于访问

· 休眠:未能在适当的时间内支付租金时所处的状态,并且在没有通过交易进行重新传播的情况下不能通过智能合约直接访问

· 唤醒:这是将休眠状态恢复到活跃状态的动作,以便可以再次通过智能合约直接访问它

DeltaDB 重新传播和租赁行为

合约的每一个状态,包括它自己的字节码,都有一个通过区块高度表示的租金计时器。一旦该计时器值为0,就会从活跃状态切换到休眠状态,并且节点可以安全地从其内部数据库中删除与该状态相关的大部分数据。当访问或修改状态时,会隐性地进行租金支付,这会被计入至该操作的gas开销中。当通过访问数据进行租金支付时,该状态会将其计时器重置为RENT_TERM。无法通过预付款的方式将一个状态的计时器设置为大于RENT_TERM的值。

使用DeltaDB当前的共识模型时,读取一个状态(通常)不会向DeltaDB证明树添加新的delta(状态更改/通知)。使用本文提出的存储区租金提案,每个状态访问都会通过向DeltaDB证明树提交一个delta从而引起状态的“重新传播”。虽然这对合约甚至大多数区块链开发人员而言都没有影响,但还是会带来许多副作用:

· SPV(轻钱包)节点可以证明状态最近一次租金支付的时间

· 相反,它允许可以从SPV或全节点的内部数据库中删除状态和大多数证明开销的证明

· SPV节点可以更快地获得状态数据的抗审查证明,通过更频繁地传播合约中最常用的数据,需要扫描的区块也更少

· 除了唤醒状态所需的开销之外,这不会消耗额外的区块空间,因为DeltaDB证明树会以单个32字节长的哈希值的形式保存在区块头中,而不会带来其他的开销

· 由于能够证明不再需要比RENT_TERM更旧的数据, 这可以大大降低Qtum-x86区块链理论上的最大磁盘空间消耗,尤其是在进行修剪操作时。通过修剪,经过500个区块后,大多数唤醒状态下的交易就不再需要存储了

当然,这将限制节点存储的数据仅限于持续共识所需的数据。区块链上的证明总是可用的,例如出版证明等用例。然而,这些证明通常只会被添加一次,并且之后也是偶尔才会被访问,因此对于共识而言是非必要的。

对于每个休眠状态,节点需要记录以下数据:

· 状态最后一次传播所处的区块高度(即最后一次支付租金的时间)

· 索引数据的密钥哈希

智能合约

大多数关于存储租赁设计的提案都要求智能合约具有明确且易错的租金管理和意识。在这种设计中,一切都是隐性的,在特定合约设计之外不需要进行租金检测操作。通过这个提案,一定程度上会对不可避免的异常行为产生影响,包括合约试图访问休眠状态时抛出的异常。

· 与以太坊的异常模型会消耗所有的gas不同,该机制只会消耗合约产生异常时的那部分gas,以及一定的“异常税”

· 所有修改后的状态都会被恢复,这点与以太坊的异常模型类似,并且这些被恢复的状态不会在DeltaDB中传播

· 在发生异常之前访问的所有活跃状态都会有租金支付,因此这些状态会在DeltaDB中传播

· 如果活跃状态被修改了并且实际执行过程中从未读取过该状态,则状态不会有租金支付,因此也不会在DeltaDB中传播。如果执行没有以异常结束,则将传播修改后的新状态

· 如果执行附加了唤醒状态,则此状态会被标记为“已访问”,因此即使在执行中出现异常,该状态仍会在DeltaDB中传播并恢复。请注意,恢复状态下存在“唤醒税”,必须在合约执行开始前支付。如果发送到账户的用于支付唤醒税的gas数太少,则将不会进行任何恢复操作,除了返回表明执行失败的收据之外,不会执行其他的操作并且所有gas都会被消耗掉

· 如果执行附加了唤醒状态,但该状态已经处于被唤醒的状态,那么这个已经处于唤醒状态的状态将被忽略,并且也不会消耗任何gas。这使得那些为确保合约成功执行而谨慎地加入即将到期的唤醒状态的人不必支付成本。附加的休眠状态将被唤醒并需支付唤醒税

· 在上述这些被附加的状态已经处于唤醒状态的情况下,该状态会被认为是已访问的,因此会在DeltaDB中传播并且INDEX_TAX + PROP_TAX将按状态键收费

这种隐性租金支付和异常设计方案意味着大多数合约完全不需要担心租赁机制的正常运作。但是,对于那些需要对租赁机制有一些自我意识的合约,则需要添加一些额外的系统接口:

uint32_t remainingRent(uint8_t * key,size_t keylen); -- 针对区块而言,将返回特定状态键所需支付的剩余租金。如果状态处于休眠状态或尚未写入,则返回0

uint32_t remainingExternalRent(UniversalAddressABI * target,uint8_t * key,size_t keylen); -- 与remainingRent方法的行为相同,但作用于外部合约

uint32_t remainingExternalBytecodeRent(UniversalAddressABI * target); -- 与remainingExternalRent方法的行为相同,但该方法会检查外部合约的字节码而不是状态键。请注意,不需要内部版本,因为通过执行合约的行为,剩余的租金将始终是RENT_TERM

uint32_t BlockData-> RENT_TERM -- 这是一个区块常量(可能之后会由DGP修改),其最大租期为

GAS模型

当前Qtum-x86虚拟机中用于存储的gas模型设计还没有完全实现,不然要是实现了的话,本提议将完全地改变它。所以,现在最好是暂时放下手中的设计工作。

定义:

PROP_TAX -- 对任何添加到DeltaDB树的传播收取的税费

READ_TAX(size) -- 从节点数据库读取状态所收取的税费。这个开销不是固定不变的,可能是由最小成本加上一定长度后的每字节成本构成。这会对存储开销产生影响,例如将数据复制到VM内存中

EXEC_TAX -- 为了执行合约而初始化一个新的VM实例所收取的税费

SHORT_READ_TAX(size) -- 当前执行中读取先前从数据库读取的状态所收取的税费。其他方面与READ_TAX类似

INDEX_TAX(key_size) -- 对数据库中数据建立索引收取的税费。这设计的相对便宜,并且包括了在需要时对密钥进行哈希的成本

WRITE_TAX(size) -- 将状态写入数据库而收取的税费

EXTERNAL_TAX -- 访问外部账户状态的一小笔额外税费

LIBEXEC_COST -- 执行任何可信库合约的固定成本。请注意,为防止滥用,可信库合约的大小存在严格限制

STORE_REFUND(size) -- 假设状态减少为0,则因修改状态而给予的退款

DIRTY_STORE_REFUND(old_size) -- 与STORE_REFUND类似,但如果状态减少为0并且它是在当前执行中创建的(即,它从未写入数据库),则返回一笔更大金额的退款

PROP_REFUND -- 如果状态在执行开始时未建立,而在执行期间建立,且在执行完成之前减少到0,则给予退款,这意味着不需要进行传播。这只适用于以null状态作为开始状态和结束状态的修改。即,如果状态切换过程为“abc” - > 0 - >“abc”,则仍将收取PROP_TAX的费用,但如果状态切换过程为0 - >“abc” - >“xyz” - > 0,则将会退款

CLEANING_REFUND(size) -- 这是额外的退款,用来激励对存储进行清理。仅在状态重置为0时有效,而在状态调整时不会给予退款

WAKE_TAX(size) -- 将状态恢复为“活跃”状态的额外开销

SLEEPING_REFUND -- 打破休眠状态的固定退款

实际操作

在阅读时请注意:

初始化合约执行:PROP_TAX + READ_TAX(size)+ EXEC_TAX

首次执行外部合约:PROP_TAX + READ_TAX(size)+ EXEC_TAX + EXTERNAL_TAX

第二次执行外部合约:SHORT_READ_TAX(size)+ EXEC_TAX + EXTERNAL_TAX

递归地执行合约:SHORT_READ_TAX(size)+ EXEC_TAX

合约自我销毁:PROP_TAX + STORE_REFUND(size)+ CLEANING_REFUND(size)

内部首次大小检查:PROP_TAX + INDEX_TAX(key_size) - 这可用于强制支付(便宜的)租金而不用将状态实际读入内存;当前这只是读取一个0字节长的状态。(状态读取通常返回数据的实际大小)

外部首次大小检查:PROP_TAX + INDEX_TAX(key_size)+ EXTERNAL_TAX

内部第二次大小检查:INDEX_TAX(key_size) -- 这里的第二次表示发生在先前的大小检查或状态读取之后

外部第二次大小检查:INDEX_TAX(key_size)+ EXTERNAL_TAX

内部首次读取:PROP_TAX + READ_TAX(size)+ INDEX_TAX(key_size)

内部第二次读取:SHORT_READ_TAX(size)+ INDEX_TAX(key_size) -- 读取在同一执行过程中写入的状态

首次写入新状态:PROP_TAX + WRITE_TAX(size)+ INDEX_TAX(key_size)

首次写入新状态,设置为0:INDEX_TAX(key_size) - 这是一个空操作,所以正常情况下不应该执行

第二次写入新状态:WRITE_TAX(size)+ STORE_REFUND(size)+ INDEX_TAX(key_size)

第二次写入新状态,设置为0:DIRTY_STORE_REFUND(old_size)+ INDEX_TAX(key_size)+ PROP_REFUND

首次写入现有状态:PROP_TAX + WRITE_TAX(size)+ STORE_REFUND(old_size)+ INDEX_TAX(key_size)

首次写入现有状态,设置为0:PROP_TAX + STORE_REFUND(old_size)+ INDEX_TAX(key_size)+ CLEANING_REFUND(size)

第二次写入现有状态:WRITE_TAX(size)+ STORE_REFUND(old_size)+ INDEX_TAX(key_size)

第二次写入现有状态,设置为0:PROP_TAX + STORE_REFUND(old_size)+ INDEX_TAX(key_size)+ CLEANING_REFUND(size) - 与首次写入相同

第二次写入先前在第一次写入时设置为0的现有状态:WRITE_TAX(大小)+ INDEX_TAX(key_size) - 基本上与正常的第二次写入相同

首次写入休眠状态:PROP_TAX + WRITE_TAX(size)+ INDEX_TAX(key_size) - 请注意,在这种情况下,减少数据无法给与退款,但WAKE_TAX预期会高于退款金额。另请注意,第二次写入与写入正常的现有(脏)状态相同

首次写入休眠状态,设置为0:PROP_TAX + INDEX_TAX(key_size)+ SLEEPING_REFUND - 理论上这应该与平均键大小(小于32字节)四舍五入后的值相抵消

注意:在第一次写入之后,休眠状态在设置为0、调整大小等方面会被视为与其他状态的行为相同

外部首次读取:PROP_TAX + READ_TAX(size)+ INDEX_TAX(key_size)+ EXTERNAL_TAX

外部第二次读取:SHORT_READ_TAX(size)+ INDEX_TAX(key_size)+ EXTERNAL_TAX

独立代码执行:EXEC_TAX - “独立”执行只是一段UTXO中的代码,执行一次而不对状态进行存储,因此除了执行之外没有任何其他成本

休眠状态的预执行恢复:WAKE_TAX(size)+ INDEX_TAX(size)+ PROP_TAX - 请注意,这是在执行原始合约之前发生的

注意:恢复休眠状态后,所有gas成本与正常的活跃状态的存储相同

可信库执行:PROP_TAX + LIBEXEC_COST - 这显然不会带来每字节长度的开销。除了实际执行代码所需的gas成本之外,该执行的成本是固定的。这是为了使可信库执行更具可预测性

注意:代表合约的所有受信任库读/写与正常的合约执行相同,不会带来EXTERNAL_TAX

虽然这个操作列表看起来非常大,但实际上它是非常公式化的,并且在代码的实现过程中不会太难。它是非常有规则的,应该只需要处理很少的边界情况。上面定义的每个常量或方法应该是不言自明的,并且应该考虑到节点和更大网络所需的所有成本。

这种方法的一些风险在于退款必须是保守的,以避免出现下面这种投机取巧的情况:例如,先将数据写入状态,然后将状态修改为较小的大小,而不是在开始简单地就写入较小的状态。退款行为与以太坊不同,执行操作后的任何剩余的gas都会被发送回收款人,其中数量不超过发送给合约的总gas数。如果允许发送回多于合约中发送的gas数,那么可以人为地利用高的gas价格输出Qtum,从而以比初始支付时更高的gas价格进行退款。

AAL账户抽象层修改

为了适当地修剪合约交易中的无关数据,所有的合约执行和交易创建都将经由AAL支出并进行压缩。这也会极大地简化将来其他的QIPs,例如基于UTXO模型的“一次性拥有”状态的提案。目前,合约执行仅在执行中的资金实际用于智能合约时才由AAL支出。此外,合约创建交易仅在合约自毁时花费。这允许SPV节点利用一些额外的功能来跟踪合约行为,但这会以在 UTXO集中保留重复且不太相关的数据为代价。DeltaDB中的SPV目标访问和跟踪方法将有效地取代此功能。

新的节点分类

目前,Qtum生态系统中有三种主要类型的节点:

1. 存档节点 :该类节点包含整个区块链的数据。UTXO集被修剪至不包含重复数据,但所有已花费的交易数据会保存在磁盘上,数据存取较慢。该类节点可用于任何用例,包括委托,常规钱包,历史数据分析,开发等。

2. 修剪的全节点: 该类节点类似于一个全节点,会下载并验证整个区块链,但会删除那些可证明不被使用的数据。特别地,这包括已花费的交易数据以及旧的区块数据。除历史数据分析外,该节点能够处理全节点的所有用例。

3. SPV节点 :这种类型的节点通过按需下载与当前钱包“相关”的数据以及整个区块链的区块头来进行验证和证明。该类节点是非常轻量级的,通常用于移动设备和“快速同步”的钱包。节点是去中心化的,但会受审查的影响,因为无法证明它所连接的全节点是否具有应该存在的数据。通常认为这种最终的安全性是稳定的,但容易受到女巫攻击。这种类型的节点通常仅可用于钱包和一些有限类型的智能合约的开发。值得注意的是,它不能用于委托。

基于本提案提出的新功能和可证明的行为,提出了一种新的节点分类方案:快速开发节点。该类节点使用了SPV节点的通用安全范例,并且初始时需要下载以下数据:

· 使用最佳区块状态树根节点的完整EVM数据(这是无法避免的)

· 区块链的所有区块头(与SPV相同)

· DeltaDB证明以及那些最近RENT_PERIOD区块的数据(根据区块头的DeltaDBRoot进行验证)。修改后的数据可以在处理时进行修剪

· 相关的UTXOs和当前受控钱包的证明(与SPV相同)

按需下载的数据包括:

1. 为新区块委托UTXO

2. 用于证明UTXO存在性的UTXO证明(即区块哈希和merkle路径),然后接受区块将其作为委托花费

3. 需要已花费的用于委托UTXO的UTXOs的证明(与SPV节点相同)

4. 需要已花费且为相关地址创建的UTXOs的证明(与SPV节点相同)

5. 需要与合约交互并跟踪RENT_PERIOD内的DeltaDB状态变化的交易(不仅仅是UTXO)。交易数据在执行后被修剪,只留下DeltaDB跟踪数据

6. 正在进行的区块头下载

对于委托和安全性这类关键目的而言,这种方案并不是安全的,因为它的核心安全性仍然是由SPV保证的。然而,对于智能合约开发而言这已经完全足够了,同时也可作为SPV节点的一个功能更强大的版本。历史合约执行可以忽略,但进行中的新合约执行可以完全地被执行和跟踪。最初的同步过程只会比SPV慢一点,主要是因为需要下载完整的EVM和修剪的DeltaDB数据。带宽成本也只比SPV节点略高,主要用于完整地下载智能合约执行所涉及到的所有交易。

原理

这种特定的租赁实现方式不同于现有的已提出的大多数方案。这其中一个最大的担忧是,智能合约本身已经相当复杂了,租赁方案所带来的额外的复杂性会大大增加智能合约代码中的问题和漏洞利用的可能性。该提案以一种不同的方式利用DeltaDB的特性从而使租赁系统对生态系统有益,而在大多数情况下不需要任何额外的智能合约逻辑。

此外,该提案的一个重点是将节点达成共识的所需和其他内容分离开来。将那些很少访问且永远不会更新的数据上链是完全可以接受的,但这些数据对节点而言应该是无关的。当然,仍然可以证明数据在某个区块高度时在区块链上的存在性,但是,预计它不会被网络上的大多数节点直接存储和访问。

这种证明可以在不消耗任何会带来gas开销的区块链资源的情况下完成。此类历史数据可以转移到归档节点上。对于仅需要访问某个智能合约的休眠数据的应用程序,可以使用部分归档节点。这基本上是一个标准的修剪节点,但它会存储相关智能合约的完整历史数据。

策略

这将在Qtum-x86的初始版本中实现。在发布后更改该存储模型是非常困难的,因此,在已经实现的情况下推出Qtum-x86是有很大好处的。

待实现

· 需要计算不同的RENT_TERM值的理论上的数据上限

· 需要计算对于一个合约执行的完整区块而言,DeltaDB merkle树的大小,以及一个典型的区块

· 这并不能完全消除对存储所有数据的“归档节点”的需求。为了无信任地同步一个全节点,仍然必须且/或需要从区块数据重建所有的“休眠”数据,以便证明区块链的当前状态是有效的。

为了实现更好的区块链技术与互信商业之间的桥梁,Qtum量子链不断在努力,持续的技术更新迭代和提出新的可实现的技术思考。(Qtum)