

NEAR扩容方案最新技术进展

我们很高兴向大家宣布,2.4.0版本的Aurora EVM引擎已经发布了。本次发布将对我们的部分合作伙伴以及Aurora拓展以太坊生态的使命产生重要影响,具体我们会在下文予以介绍。
Aurora基于NEAR区块链开发,后者和以太坊1.0在很多方面都有着不同之处。这里想强调的是NEAR为每笔交易能做的事情的数量做了限制,以太坊则只为每个区块设立了限制(用户可以为他们的独立交易设置限制)。因此,理论而言,Aurora必须将整个以太坊区块信息打包进单笔NEAR交易中。这是个不小的挑战,但随着新版本引擎的发布,我们实现了性能的部分优化,这距离我们实现上述目标又近了一步。
很多DeFi项目都遇到了超出每个合约允许使用的gas最大值的错误。并且由于认为这里的“gas”和“合约”指的是以太坊中的概念而带来了更多疑惑。然而,这种错误实际上来自NEAR交易:gas指的是NEAR上的 gas,而不是EVM中的 gas,合约指的是Aurora引擎在NEAR上的智能合约。说到这里,这种错误信息就已经很清楚了。每个合约允许单笔交易销毁的gas数量会有一个最大值,我们的引擎在运行这些DeFi交易的过程中正在超过这个最大值。
随着新版本的发布,这个问题正在慢慢成为历史。我们将引擎的gas用量降低了两倍,为开发者带来一些十分重要的用例,同时由于意外触达极值的难度增加了两倍,用户体验也因此大为提升。尤其是适用于单笔交易的EVM计算量已足够解锁部分DeFi用例,这对以前的Aurora来说是不可能的。
我们想着重表扬我们的DeFi伙伴Aurigami,是他们投入开发资源,帮助我们发现一些优化机会,让此次新版本得以顺利发布。有了新版本之后,Aurigami便可以在Aurora的测试网发布了。因为它是一个借贷平台,此次更新是他们能够尽可能丝滑地运行的一个核心要求。此次更新从全局来看意义更为重要,因为这种平台对Aurora生态的去中心化金融基础设施是至关重要的。我们很高兴看到我们的生态能够继续成长,并期待未来发布更多更新,让Aurora产生更多的大家已熟悉和喜爱的以太坊DeFi用例。
如果您还在继续浏览,那么接下来的内容一定不会让您失望!
我们先快速盘点一下Aurora引擎是什么及其工作原理。Aurora引擎是一种使用Rust编写的、基于NEAR开发的智能合约。它包含一个完整的EVM编译器,能够和以太坊一样执行交易,同时具备在执行前(查看签名、nonce、账户余额与gas价格等)验证交易所需的全部辅助逻辑。
当你向Aurora RPC端点发送一笔交易时,我们的基础架构会将你已签名的以太坊交易打包进一笔NEAR交易中,这意味着每笔Aurora交易都会成为一笔NEAR交易,并因此必须遵守NEAR协议的规则。正如上述总结,这也是NEAR gas问题产生的原因。
这就产生了一个问题:Aurora引擎如何提升效率,从而能够在同样的NEAR gas数量下承担更多的工作?这个问题并不容易解决(事实上我们也在不断优化),但对Aurora的成功至关重要。幸运的是,NEAR核心开发者和Aurigami开发者愿意帮助我们解决这个问题。
实际上,开发人员发现仅需几个简单的操作,就能对上述问题的解决产生重要影响。
更新至最新版本的Rust。Rust编译器擅长优化代码性能,而且变得越来越优秀。仅需更新至最新版本的Rust就能以近乎零成本的方式将Aurora引擎的性能提升约1%。
在EVM栈使用小端序的数字呈现形式。从技术上讲,EVM被定为使用大端编码,不过绝大多数现代框架都使用小端编码。为了进行运算操作而在栈内外不断切换数字的字节顺序会产生高昂成本。在栈上使用小端编码并且仅在必要情况下切换至大端编码(写入存储或返回结果),成本则会降低很多。
缩短检查账户是否为空的流程。如果某以太坊账户的nonce和余额都是0,且没有部署代码,那么该账户就会被视作是空的。当然一旦这两个条件中的任何一个没有被满足,我们都无需再查看其它条件。
使用引擎合约(#438) (#446)读取NEAR状态的缓存值。这一改进对引擎的gas用量产生的影响最大。
所有的这些变更让Aurora引擎在执行某些DeFi交易时,消耗gas的数量比之前降低近50%。这种大幅改进足以为Aurigami等项目的用例打开大门,让它们得以在Aurora的测试网发布。
为了搞清楚为何这种缓存变更会产生如此重要的影响,我们需要知道一些关于NEAR是如何表示状态以及决定gas成本的知识。和很多区块链的做法一样,NEAR使用前缀树来存储状态,因为这样做可以创建足够简单的链上存证。NEAR的每份合约都有自己的k-v状态存储,并且后者会被填充在前缀树中。
NEAR对设置自己的gas成本十分小心,目标是确保它们能保持1秒钟的出块速度,这种近乎即时的交易确认会产生类似Web2的用户体验。为了实现这一目标,必须保证任何区块处理交易的时间不会超过1秒钟,区块中的全部交易必须符合这一时间。由于gas是计算工作的衡量标准,我们的想法很简单:确保gas成本被认证测量,一笔交易所做的1秒钟的工作对应固定数量的gas,并将限制设置为远远低于这一数值的水平。
为了保证gas成本和区块时间之间这种严格的匹配关系,我们必须要有独立成本,来对应一笔交易可能导致节点执行的全部操作。举个例子,read_base代表读取任何合约状态的成本,read_byte代表读取每个字节的基础成本之外的成本。我们再来回顾一下状态细节,其中有一个touching_trie_node成本,代表状态前缀树的每个节点由于被访问而引发的成本。
事实证明,这些IO成本构成了引擎使用的全部gas的很大一部分,在某些情况下这一比例能超过50%。因此,通过引入缓存降低状态读取的次数会让我们大为受益。虽然技术缓存逻辑本身也有成本(更不用提其存储足迹也会有成本),但和降低读取数量所节省的成本相比要小很多。
其它差异还包括,某些类型值要求不同的缓存逻辑。一个被称作generation的特殊值是引擎的一种实现细节。每个地址都会有一个与之相关联的generation。这种值允许我们删除整个地址,且不会产生大量的IO成本。我们只需增加generation的值,忽略任何和更低的generation的值相关的状态。
这意味着,从EVM合约存储(开发者从Solidity层看很像是单次读取)那里读取值将会是多次读取,因为引擎需要先读取generation。然而,generation不会在交易过程中发生改变。如果某账户在同一笔交易中被删除又被重新创建,这些变化会由EVM编译器缓存在内存中。此外,单笔交易一般不会访问到很多不同的地址,即便是复杂的DeFi交易也很少会调用超过10个以上的EVM合约。因此,我们能够缓存整个generation(随着每次读取不断映射)地址,这就是实现的全部内容。
从另一方面说,EVM合约存储锁定本身无法在不产生大量成本的情况下被完全缓存,因为这些值可能要比代表generation的32-bit数字大得多,key可能会多得多。如果一笔交易调用10份不同的合约,每份合约必须从存储读取10个值,那么value所对应的缓存的key就有100多个。
此外,目前还不清楚这种缓存是否真的有价值,因为合约存储的某个给定值可能不会被读取多次,不过我们可以保证的是地址的generation值会被需要多次。因此,我们为generation用到的同样的缓存并不适用于更宽泛的缓存。
由于EVM编译器效率低下(我们会在未来妥善解决这个问题),在Solidity层看起来很像读取值的操作在引擎层变成了连续且重复的读取值的操作。有例子证明同样的值会被连续读取3~4次!幸运的是,这个问题可以通过极其简单的、缓存大小为1的LRU缓存解决,目前我们已经实现了这一点。
恭喜大家已完成了本次阅读!希望大家喜欢我们对Aurora引擎的深入解析。我们会继续践行我们的使命,增加Aurora的用例,同时会继续Aurora的优化工作。如果您对此有兴趣并熟练掌握了Rust,可以前往我们的招聘页面,期待收到您的简历!