LID:剖析六种 EVM 语言:优秀的语言要如何设计

以太坊虚拟机(EVM)是一个256位、基于堆栈、全球可访问的图灵机。由于架构与其他虚拟机和物理机的明显不同,EVM需要领域特定语言DSL。

在本文中,我们将研究EVMDSL设计的最新技术,介绍六种语言Solidity、Vyper、Fe、Huff、Yul和ETK。

语言版本

Solidity:0.8.19

Vyper:0.3.7

Fe:0.21.0

Huff:0.3.1

ETK:0.2.1

Yul:0.8.19

阅读本文,需要你对EVM、堆栈和编程有基本的了解。

以太坊虚拟机概述

EVM是一个基于256位堆栈的图灵机。然而,在深入研究它的编译器之前,应该介绍一些功能特性。

由于EVM是「图灵完备」的,它会受到「停机问题」的困扰。简而言之,在程序执行之前,没有办法确定它未来是否会终止。EVM解决这个问题的方法是通过「Gas」计量计算单位,一般来说,这与执行指令所需的物理资源成比例。每个交易的Gas量是有限制的,交易的发起者必须支付与交易消耗的Gas成比例的ETH。这个策略的影响之一是,如果有两个功能上相同的智能合约,消耗更少Gas的合约将被更多采用。这导致协议竞争极端的Gas效率,工程师努力最小化特定任务的Gas消耗。

此外,当调用一个合约时,它会创建一个执行上下文。在这个上下文中,合约有一个堆栈用于操作和处理,一个线性内存实例用于读写,一个本地持久性存储用于合约读写,并且附加到调用的数据「calldata」可以被读取但不能被写入。

关于内存的一个重要说明是,虽然它的大小没有确定的「上限」,但仍然是有限的。扩展内存的Gas成本是动态:一旦达到阈值,扩展内存的成本将呈二次方增长,也就是说Gas成本与额外内存分配的平方成正比。

合约也可以使用一些不同的指令来调用其他合约。「call」指令将数据和可选的ETH发送到目标合约,然后创建自己的执行上下文,直到目标合约的执行停止。「staticcall」指令与「call」相同,但增加了一个检查,即在静态调用完成之前,断言全局状态的任何部分都未被更新。最后,「delegatecall」指令的行为类似于「call」,只是它会保留先前上下文的一些环境信息。这通常用于外部库和代理合约。

为什么语言设计很重要

在与非典型架构交互时,特定领域语言是必要的。虽然存在诸如LLVM之类的编译器工具链,但是依赖它们来处理智能合约,在程序正确性和计算效率至关重要的情况下,不太理想。

程序正确性非常重要,因为智能合约默认是不可变的,并且鉴于区块链虚拟机的属性,智能合约是金融应用程序的热门选择。虽然存在针对EVM的升级性解决方案,但它充其量只是一个补丁,最坏的情况是任意代码执行漏洞。

计算效率也非常关键,因为最小化计算具有经济优势,但不能以安全为代价。

简而言之,EVMDSL必须平衡程序正确性和Gas效率,在不牺牲太多灵活性的情况下通过做出不同的取舍来实现其中之一。

语言概览

对于每种语言,我们将描述它们的显着特性和设计选择,并包括一个简单的计数功能智能合约。言语流行度是根据DefiLlama上的总锁定价值(TVL)数据确定的。

Solidity

Solidity是一种高级语言,其语法类似于C、Java和Javascript。它是按TVL计算最受欢迎的语言,其TVL是第二名的十倍。为了代码重用,它使用面向对象模式,智能合约被视为类对象,利用了多重继承。编译器采用C++编写,计划在将来迁移到Rust。

可变的合约字段存储在持久性存储中,除非它们的值在编译时或部署时已知。合约内声明的方法可以声明为pure、view、payable,或默认情况下是non-payable但状态可修改。pure方法不会从执行环境中读取数据,也不能读取或写入持久性存储;也就是说,给定相同的输入,pure方法将始终返回相同的输出,它们不会产生副作用。view方法可以从持久性存储或执行环境中读取数据,但它们不能写入持久性存储,也不能创建副作用,例如附加事务日志。payable方法可以读写持久性存储,从执行环境中读取数据,产生副作用,并且可以接收附加在调用中的ETH。non-payable方法与payable方法相同,但具有运行时检查,以断言当前执行上下文中没有附加ETH。

注意:将ETH附加到交易中与支付Gas费用是分开的,附加的ETH由合约接收,可以通过恢复上下文选择接受或拒绝它。

在合约的范围内声明时,方法可以指定以下四种可见性修饰符:private、internal、public或external。private方法可以通过当前合约内的「jump」指令在内部访问。任何继承的合约都不能直接访问private方法。internal方法也可以通过「jump」指令在内部访问,但继承的合约可以直接使用内部方法。public方法可以通过「call」指令由外部合约访问,创建一个新的执行上下文,并在直接调用方法时通过跳转进行内部访问。public方法也可以通过在方法调用前加上「this.」来在新的执行上下文中从同一合约中访问。external方法只能通过「call」指令访问,无论是来自不同的合约还是在同一合约内,都需要在方法调用前加上「this.」。

注意:「jump」指令操作程序计数器,「call」指令为目标合约的执行期间创建一个新的执行上下文。在可能的情况下,使用「jump」而不是「call」更加节约Gas。

Solidity还提供了三种定义库的方式。第一种是外部库,它是一个无状态的合约,单独部署到链上,在调用合约时动态链接,并通过「delegatecall」指令访问。这是最不常见的方法,因为外部库的工具支持不足,「delegatecall」很昂贵,它必须从持久存储中加载额外的代码,并且需要多个事务进行部署。内部库的定义方式与外部库相同,只是每个方法必须定义为内部方法。在编译时,内部库被嵌入到最终合约中,并且在死代码分析阶段,库中未使用的方法将被删除。第三种方式与内部库类似,但不是在库内定义数据结构和功能,而是在文件级别定义,并且可以直接导入和在最终合约中使用。第三种方法提供了更好的人机交互性,可以使用自定义数据结构,将函数应用于全局作用域中,并一定限程度上将别名运算符应用于某些函数。

编译器提供两个优化通道。第一个是指令级优化器,对最终的字节码执行优化操作。第二个是近期增加使用Yul语言作为编译过程中的中间表示,然后对生成的Yul代码进行优化操作。

为了与合约中的公共和外部方法交互,Solidity规定了一种应用程序二进制接口标准来与其合约交互。目前,SolidityABI被视为EVMDSL的事实标准。指定外部接口的以太坊ERC标准都按照Solidity的ABI规范和风格指南来执行。其他语言也遵循Solidity的ABI规范,很少出现偏差。

Solidity还提供了内联Yul块,允许对EVM指令集进行低级别访问。Yul块包含Yul功能的子集,详细信息请参见Yul部分。这通常用于进行Gas优化,利用高级语法不支持的功能,并自定义存储、内存和calldata。

由于Solidity的流行,开发人员工具非常成熟且设计精良,Foundry是在这方面突出的代表。

以下是用Solidity编写的一个简单合约:

Vyper

Vyper是一种语法类似于Python的高级语言。它几乎是Python的一个子集,只有一些小的不同。它是第二受欢迎的EVMDSL。Vyper针对安全性、可读性、审计能力和Gas效率进行了优化。它不采用面向对象模式、内联汇编,并且不支持代码重用。它的编译器是用Python编写的。

存储在持久性存储器中的变量是在文件级别声明的。如果它们的值在编译时已知,可以将它们声明为「constant」;如果它们的值在部署时已知,则可以将它们声明为「immutable」;如果它们被标记为public,则最终合约将为该变量公开一个只读函数。常量和不变量的值通过它们的名称在内部访问,但是持久性存储器中的可变量可以通过在名称前面添加「self.」来访问。这对于防止存储变量、函数参数和局部变量之间的命名空间冲突非常有用。

和Solidity类似,Vyper也使用函数属性来表示函数的可见性和可变性。被标记为「@external」的函数可以通过「call」指令从外部合约访问。被标记为「@internal」的函数只能在同一合约中访问,并且必须以「self.」为前缀。被标记为「@pure」的函数不能从执行环境或持久存储中读取数据,也不能写入持久存储或创建任何副作用。被标记为「@view」的函数可以从执行环境或持久存储中读取数据,但不能写入持久存储或创建副作用。被标记为「@payable」的函数可以读取或写入持久存储,创建副作用,接受收ETH。没有声明这个可变性属性的函数默认为non-payable,也就是说,它们和payable函数一样,但不能接收ETH。

Vyper编译器还选择将局部变量存储在内存中而不是堆栈上。这使得合约更加简单和高效,并解决了其他高级语言中常见的「堆栈过深」的问题。但是,这也带来了一些折衷。

另外,由于内存布局必须在编译时知道,因此动态类型的最大容量也必须在编译时知道,这是一个限制。此外,分配大量内存会导致非线性的Gas消耗,正如EVM概述部分中提到的。但是,对于许多用例来说,这个Gas成本可以忽略不计。

虽然Vyper不支持内联汇编,但它提供了更多内置函数,以确保几乎每个Solidity和Yul中的功能在Vyper中也可以实现。通过内置函数可以访问低级位运算、外部调用和代理合约操作,通过编译时提供覆盖文件可以实现自定义存储布局。

Vyper没有丰富的的开发工具套件,但它有更紧密集成的工具,并且也可以插入到Solidity开发工具中。值得关注的Vyper工具包括Titanaboa解释器,它具有许多与EVM和Vyper相关的内置工具,可用于实验和开发,以及Dasy,一种基于Vyper的Lisp,具有编译时代码执行功能。

下面是用Vyper编写的一个简单合约:

Fe

Fe是一种类似Rust的高级语言,目前正在积极开发中,大部分功能尚未推出。它的编译器主要用Rust编写,但使用Yul作为其中间表示形式,依赖于用C++编写的Yul优化器。随着Rust原生后端Sonatina的加入,这一点有望改变。Fe使用模块进行代码共享,因此不使用面向对象的模式,而是通过基于模块的系统重用代码,在模块内声明变量、类型和函数,可以以类似于Rust的方式进行导入。

持久存储变量在合约级别声明,如果没有手动定义的getter函数则不可公开访问。常量可以在文件或模块级别声明,并且可以在合约内部访问。当前不支持不可变的部署时变量。

方法可以在模块级别或合约内声明,默认是pure和private。要使合约方法公开,必须在定义前加上「pub」关键字,这使得它可以在外部访问。要从持久化存储变量中读取,方法的第一个参数必须是「self」,在变量名前加上「self.」,使该方法具有只读访问本地存储变量的权限。要读取和写入持久化存储,第一个参数必须是「mutself」。「mut」关键字表示合约的存储在方法执行期间是可变的。访问环境变量是通过将「Context」参数传递给方法来完成的,通常命名为「ctx」。

函数和自定义类型可以在模块级别声明。默认情况下,模块项都是私有的,除非加上「pub」关键字才能访问。但是,不要和合约级别的「pub」关键字混淆。模块的公共成员只能在最终合约或其他模块内部访问。

Fe暂时不支持内联汇编,相反,指令由编译器内部函数或在编译时解析为指令的特殊函数包装。

Fe遵循Rust的语法和类型系统,支持类型别名、带有子类型的枚举、特征和泛型。目前这方面的支持还有限,但正在进行中。特征可以针对不同类型进行定义和实现,但不支持泛型,也不支持特征约束。枚举支持子类型,并可以在其上实现方法,但不能在外部函数中对其进行编码。尽管Fe的类型系统仍在发展中,但它在为开发人员编写更安全、编译时检查的代码方面显示出了很大的潜力。

下面是用Fe编写的一个简单的合约:

Huff

Huff是一种汇编语言,具有手动堆栈控制和对EVM指令集的最小化抽象。通过「Billions项目组definefn」指令定义,可以接受模板参数以提高灵活性,并指定函数开始和结束时的预期堆栈深度。由于这些函数是内部的,因此无法从外部访问,在内部访问需要使用「jump」指令。

其他控制流程,例如条件语句和循环语句可以使用跳转目标定义。跳转目标是由标识符后跟冒号定义的。可以通过将标识符压入堆栈并执行跳转指令来跳转到这些目标。这在编译时解析为字节码偏移量。

宏由「Billions项目组、Swift和Kotlin到Solidity、Sway和Cairo。学习在这些语言之间无缝切换为软件工程职业提供了无与伦比的灵活性。最后,重要的是要了解每一种语言背后都需要付出大量的工作。没有人是完美的,但无数有才华的人付出了大量努力,为像我们这样的开发者创造安全愉快的体验。

郑重声明: 本文版权归原作者所有, 转载文章仅为传播更多信息之目的, 如作者信息标记有误, 请第一时间联系我们修改或删除, 多谢。

链链资讯

币赢DNI:美国政府接管硅谷银行

在围绕硅谷银行的流动性闹剧中,美国正在介入并接管陷入困境的金融机构。 Coinbase或将向美国政府机构出售区块链分析软件:金色财经报道,公开记录显示,Coinbase希望将其区块链分析软件出售.

[0:31ms0-3:433ms