Move是一种新型的智能合约语言,被包括 Aptos Network 在内的几个区块链所采用。Move最初是为Meta的Libra/Diem区块链设计的,它基于“安全优先”原则,这使得它成为市场上智能合约可能最安全的语言。然而,这种方法导致了一个“极简主义”的语言,省略了许多高级语言特性,这些特性可以简化开发者的工作。在Aptos Labs,我们正在开发一个新的 Move 编译器——Aptos Move 编译器,它带来了一系列新的语言特性,填补了原始Move语言设计的空白,所有这些都不会影响安全性。在本文中,我们概述了一些最重要的即将推出的特性。这些特性中的许多还没有完全确定:我们提前预览分享,是为了在我们开始实施它们时征求社区的反馈。
接收器风格函数调用
接收器风格函数调用语法是一种众所周知的符号,其中调用的目标(“self”)与函数名称和其余参数分开,如receiver.func(args)。这种符号可以被看作是func(receiver, args)的语法替代品。然而,这种符号的便利性还有一些额外的内容:
- 函数
func不需要被显式地导入或由定义模块限定,因为接收器的类型,第一个参数,决定了它。 - 如果
func的接收器参数是引用,编译器可以自动创建这个引用。例如,func(&mut receiver, args)可以以接收器风格写成receiver.func(args)。
Aptos Move编译器将实现这种符号。通过使用特定的命名约定来启用它,用于常规函数定义的第一个(接收器)参数,如下所示:
fun length<E>(self: &vector<E>)这里,self不是关键字,而是对编译器的指示,表明这个函数可以被调用为vec.length()。注意,这并不禁止当前的符号length(&vec),它仍然被支持,允许将现有代码升级到新符号,而不需要破坏性更改。
有了接收器风格,允许受限的函数声明重载,只要它们通过接收器参数的类型来区分。因此,以下声明在一个模块中是允许的:
fun name(self: &T): String { self.name }
fun name(self: &R): String { self.other_name }一等高阶函数
Aptos 在 2023 年初为 Move 添加的一个特性是对高阶函数和 lambda 的有限支持。例如,Move 现在支持以下符号,它检查一个向量是否包含一个值大于零的元素(注意,我们假设接收器风格,如上文讨论):
vec.contains(|elem| elem > 0)然而,当前对高阶函数的支持是有限的:只有内联函数可以作为参数接受函数,并且只有 lambda 表达式可以传递给这些函数参数。内联函数的一个严重限制是它们不能跨模块封装边界工作,这可能会诱使构建者牺牲模块封装。此外,使用太多的内联函数会导致代码大小增加,当部署代码时,会触及交易负载大小的限制。
Aptos Move 编译器将支持一般的高阶函数,Move VM 也将扩展以支持它们。实际上,不会发生语法变化,但高阶函数将不再仅限于内联函数。例如,我们可以编写如下代码(由于私有字段访问,今天不可能使用内联函数):
struct State { val: u64 }
public fun transform(self: &mut State, f: |u64|u64) {
s.val = f(s.val)
}这个新特性有两个挑战:
- 可重入性:一般高阶函数打开了可重入攻击的问题。我们有一个解决方案,通过资源访问控制,如后文讨论。
- 闭包和引用:闭包是捕获 lambda 引用的上下文变量的构造。例如,在
let x = …; contains(vec, |elem| elem > x)中,x就是这样一个上下文变量。如果需要捕获引用,这种捕获就会变得困难。解决这个问题的方法是只允许捕获引用的闭包向下在调用栈上传递。这确保了闭包的生命周期被封闭在捕获引用的生命周期内。
用户定义能力
许多编程语言具有特性(即,有界参数多态性)的概念,允许将通用特性与类型关联起来,例如该类型的排序。这种多态性允许优雅的代码重用,例如编写适用于具有排序的任何类型的代码。在Move中,我们已经具有相关的概念,能力。这些被限制在少数预定义用例中,如复制、密钥、存储和丢弃能力。随着即将到来的 Aptos Move 编译器,我们计划扩展现有的 Move 能力系统,允许用户在语言中定义新的能力。
可以像下面这样声明能力,我们使用迭代器能力作为例子。为了简单起见,我们描述的迭代器通过一个简单的 API 消耗它迭代的元素:
ability Iterator<E> {
fun has_more(self: &Self): bool;
fun next(self: &mut Self): E;
}给定这样的能力,可以在具有此能力的任何类型上实现通用算法,例如搜索迭代器中的匹配元素:
fun any<E, I: Iterator<E>>(iter: &mut I, predicate: |&E|bool): bool) {
while (iter.has_more()) {
if (predicate(&iter.next()) return true;
}
false
}在上面的代码中,I: Iterator<E> 意味着多态约束 I 绑定到只有具有这种能力的那些类型。
可以像下面这样声明能力实现:
fun <E>Iterator<E>::has_more(self: &vector<E>): bool { !v.is_empty() }
fun <E>Iterator<E>::next(self: &mut vector<E>): E { self.pop().unwrap() }我们也可以派生能力:
fun <E, I1: Iterator<E>, I2: Iterator<E>>
Iterator<E>::next(self: &mut Chain<I1, I2>): E
{
if (self.first().has_more())
self.first_mut().next()
else
self.second_mut().next()
}一些更多的细节:
- 实现源:能力可以被声明为公共或非公共。两种能力在任何地方都可见。然而,一个公共能力可以为在任何地方定义的类型实现,甚至在其他包中,而一个常规能力只能在类型所在的同一包中实现(孤儿规则)。请注意,公共能力的可用性是实验性的,可能有助于为跨合约标准的发展提供更多的灵活性。比较之下,Rust中没有(但例如在Go中有)。
- 实现唯一性:对于任何给定的类型,在任何执行上下文中只能有一个唯一的能力实现。这个实现必须由一个单一的模块提供——可能在类型的包之外。这两个要求都在模块加载时进行检查。这意味着,即使类型检查器可能因为单独部署单元而没有发现它,模糊的实现永远不可能在给定的执行上下文中重合或以某种方式被覆盖。这个属性避免了能力的语义混淆,同时也防止了通过能力进行的恶意攻击。
- 动态分派:到目前为止描述的能力不启用动态分派,而是在编译时静态解析。与Rust相比,这是
dyn T类型声明和常规T类型声明之间的区别。我们仍在讨论是否应该为能力支持动态分派。如果是这样,可重入性的保护将通过资源访问控制与高阶函数类似地工作。
资源访问控制
资源访问控制(AIP-56)允许Move代码明确限制函数或交易可以访问的资源集。虽然这个特性在区块链执行和安全领域有应用,它也在语言层面上启用了各种场景,所以我们将在这里简要描述它。
访问控制说明符是对Move中现有获取声明的概括。与只是 fun f(..) acquires R,其中R是某种资源类型(具有key能力的struct)不同,人们可以区分reads R和writes R。此外,可以使用通配符和否定。例如,reads 0x42::*授予在给定地址声明的所有资源的读取访问权限,而 !writes 0x42::* 拒绝对此地址的所有资源的写入访问权限。有关更多详细信息,请参见AIP-56。
访问说明符和获取注释之间的一个重要区别是,前者是函数类型的一部分,并且可以引用当前模块之外的资源。此外,访问说明符是动态评估的,允许它们被应用于没有提及它们的代码:执行具有特定访问限制的函数将把这个限制传递给从该函数调用的所有代码。
访问说明符为安全调用未知代码提供了一个解决方案,这些调用可以跨信任边界发生——那些调用可能作为高阶函数或用户定义能力的结果是。例如,考虑一个公共函数,它接受一个函数参数。通过函数类型,我们可以确保任何具体的参数都不会进行不必要的资源访问:
module myaddress::m {
public fun do_some_work(…, callback: |u64| !write myaddress::*) …
}在上面的声明中,我们要求callback函数参数不要写入与给定模块声明在同一个地址的任何资源,防止通过回调进行可重入攻击。
返回全局引用
目前,Move不支持从常规函数调用返回对全局存储的引用。例如,我们不能编写以下代码:
fun access_resource(a: address): &Resource {
abort_if_access_invalid(a);
borrow_global<Resource>(a) // 这里会出现引用安全性错误
}随着内联函数的引入,变得可能编写上述函数,但这些只能在定义资源类型的模块中使用。这个限制使得特别编写框架代码更加困难。例如,对于Aptos Objects,很自然地通过返回引用的函数提供对对象的访问器,而不是必须让用户代码去解析地址到引用。
这种限制的原因是,在当前的Move中,返回引用的来源不能被声明。然而,我们可以使用资源访问说明符作为一个指标来推导返回引用与全局存储的关系:
fun access_resource(a: address): &Resource
reads Resource
{
borrow_global<Resource>(a) // 由于访问声明,这是可以的
}更准确地说,这种解释的规则如下:
- 一个不能从任何输入引用的类型借用的返回引用被认为是全局引用。
- 每一个全局引用必须有一个匹配的唯一访问说明符,用于引用类型可以从中借用的资源类型。
有了这些信息,一个扩展的引用安全性分析可以维持Move的所有必要的内存安全条件。在Aptos Labs,我们正在正式化并实施这个新分析。
注意,这个启发式方法扩展了已经存在的处理返回引用的方法。目前,任何输出引用都被认为是从任何输入引用借用的。这里描述的扩展处理与此规则不能应用的情况。是否足够还有待观察。技术上,从这里到Rust中发现的生命周期标签注释并不是一个巨大的步骤,但是,虽然这些是合理的,但它们可能难以使用,所以我们目前尝试在Move语言中避免它们。
枚举类型和公共结构体
在Rust语言中找到的枚举类型是定义数据变体的强大特性。对于新的Aptos Move编译器,我们计划增加对枚举的完整支持。枚举声明如下:
public enum Option<A> has copy, drop, store {
None,
Some(A)
}类似于Rust的匹配表达式将允许区分枚举:
match option {
None => 0,
Some(x) => x + 1
}如上所述,公共修饰符与枚举类型一起使用。公共枚举可以在定义它的模块之外进行匹配(否则,只允许本地匹配)。相同的公共修饰符也将对常规结构体可用,允许它们在模块外部被访问,并且也可以作为交易参数传递。还要注意枚举中位置字段的引入:这些对结构体也是允许的(反过来,枚举变体也允许命名字段)。
有了枚举类型,我们将允许类型中的递归,这在Move中目前是不允许的。具有递归的枚举类型声明必须至少有一个非递归的终止变体,如下所示:
public enum List<A> has copy, drop, store {
Nil,
Cons{ hd: A, tl: List<A> }
}枚举类型的一个常见应用是资源版本控制。例如,可以像下面这样定义一个资源:
enum AccounData has key {
V1 { <一些字段> },
V2 { <一些字段>, <额外字段> }
... 任何未来的版本可以放在这里 ...
}这允许使用相同的存储槽用于资源的新版本。像这样的应用程序需要处理匹配中缺少的变体。在编译时,编译器会在缺少变体时出错。然而,在运行时,字节码验证器允许那些缺失的匹配。只有在执行匹配并且无法处理变体时,执行才会中止。这允许向枚举中添加变体,而不会阻止已经部署的旧代码的验证。
规范语言
引入通用 lambda 和高阶函数也有助于解决规范和验证中的一个主要实际问题:处理循环不变式。有了高阶函数和能力,人们可以避免许多循环,而是使用filter-map-reduce模式。
验证器可以使用特殊的决策程序来处理filter-map-reduce,而不是进入循环不变式的复杂性。下面给出了一个简单的例子:
let sum = reduce(&vec, 0, |a, e| a + e)验证器可以通过组合reduce和lambda表达式体的知识,无需进一步帮助地验证这样的表达式。
对于更复杂的情况,lambda表达式语法被扩展,以便能够附加前条件和后条件。例如,下面在reduce表达式中使用了更复杂的函数进行聚合。同时,存在一个规范函数来模拟这种行为,所以我们可以用与lambda关联的规范块来指定这种关系:
reduce(&vec, N,
|a, e|
spec { // 指定lambda的结果
ensures result == spec_aggregate(a, e);
}
aggregate(a, e)
)其他特性
计划为Move添加多个较小的特性,这里是其中一些的列表:
- 有符号整型(
i8, i16, i32, i64, i128, i256) - 表示整数范围的类型,写作
start..end - 索引向量的更直观语法(
v[index], v[start..end], &v[index], &mut v[index],以及类型中的[T]而不是vector<T>) borrow_global<R>(addr)和borrow_global_mut<R>(addr)的更直观语法(与向量索引对齐,R[addr],&R[addr]和&mut R[addr])- 循环的新语法:
for (var in exp) body。这里,exp可以是整数范围,或任何实现了Iterator或IntoIterator能力的东西。
时间表和流程
我们预计这些语言扩展的大部分将在’24年上半年落地。新的Aptos编译器将在’23年底进入beta阶段,主要目标是与旧版本功能一致。新特性将根据我们的用户——你们——和应用程序的需求逐个添加。我们将通过Aptos改进建议(AIPs)征求对语言特性的反馈,这将允许社区讨论和影响设计。Aptos的Move有很多值得期待的地方,我们很高兴与你们一起将语言推向下一个级别!