解读 Libra Move:一种可编程资源语言

Facebook 最近发表了许可链项目 Libra,其中的最大亮点是 Move 语言。 下面我们从技术视角解读一下 “Move: A Language With Programmable Resources” 这篇白皮书,供大家参考。

为了便于理解,我们拿比特币、以太坊和 Libra 来做一个对比。

可编程货币、可编程应用与可编程资源

其实,单从白皮书的标题,就可以大概看出三个项目在设计目标上的差异。

比特币的目标是 —— 可编程货币(Programmable Money),所以白皮书标题是 “Bitcoin: A peer-to-peer electronic cash system”。

以太坊的目标是 —— 可编程的去中心化应用(Programmable dApps),在货币的基础上,扩展到更通用的领域。所以白皮书标题是:“Ethereum: a next generation smart contract and decentralized application platform”,黄皮书标题是:“Ethereum: A secure decentralized generalized transaction ledger”。

而 Libra 的设计目标恰好介于二者中间 —— 可编程资源(Programmable Resources),或者叫可编程资产

Facebook 的技术路线比较务实,没有尝试更颠覆性的创新,而是把目光聚焦在 “货币” 和 “通用应用” 之间的 “资产”,围绕解决实际问题,便于工程实现而展开。从这点来看,Libra 既不是区块链 3.0 也不是 4.0,而是区块链 1.5。但这并不代表 Libra 的目标没有挑战,事实上,实现一个可以保证资产安全性,又能够提供足够灵活性的系统,比臆想出一个解决 “不可能三角” 的永动机还要困难。

那么,“可编程货币”、“可编程应用”、“可编程资源”,这三者到底有什么不同呢?

既然都是 “可编程 XX” 句式,他们的主要区别就在于两点:

  1. 对什么编程;
  2. 如何编程。

对什么编程?

对什么编程,是指系统所描述或者抽象的,到底是现实世界中的什么东西。

比特币对 “货币” 编程

比特币系统抽象的是 “货币”,或者说是 “账本” 的概念。货币可以用一个数字来描述,也就是某一个账户的 “余额”。用户可以通过 “交易”,把一部分钱转给别人。当比特币网络接收到一笔交易的时候,每个节点都会检查交易是否合法,比如你花的是不是自己的钱,有没有足够的余额(比特币不允许透支)。当这些检查都成功后,节点会做一个简单的加减计算:在你的账户中扣减转账的数额,并在对方账户中加上同样的数量。因此,比特币唯一的功能就是记账,保证在账户彼此转账的过程中,货币的总量不会莫名其妙的增加或减少(不考虑挖矿奖励和黑洞地址等特例)。

以太坊对 “应用” 编程

以太坊系统抽象的是 “应用”,应用的种类包罗万象,比如游戏、借贷系统、电商系统、交易所等,这些都是应用。理论上讲,任何传统的计算机程序都可以移植到以太坊上。因此,以太坊中记录的是各种应用的内部数据(即 “合约状态”),比如一个电商系统的库存、订单、结算信息等。这些信息无法用一个简单的数字来描述,必须允许用户定义非常复杂的数据结构,并且允许用户通过代码(智能合约),来对这些数据进行任意所需的操作。当然,这些应用也包含了 “货币账本”。事实上,目前在以太坊上应用最广泛的正是此类应用(称为 “ERC20 智能合约”)。由于以太坊把这类应用看作是平台所能支持的多种应用中的一种,与其他类型的应用相比,并没有什么特别之处,所以也就没有针对此类应用提供更多的安全保护,只提供了类似 ERC20 这样的接口规范。一个在以太坊上新发行的 “货币”,其转账逻辑的正确性完全由开发者负责。

以太坊账户存储

在以太坊的存储结构中,ERC20 代币的账本是 “二级对象”,和 ETH 原生代币余额存储在不同的地方。例如上图所示,0x0,0x1 和 0x2 是三个以太坊地址,其中,0x0 和 0x2 是普通账户地址(External accounts),而 0x1 是一个合约地址(Contract accounts)。我们可以看到,每个账户都存储了一个 ETH 的余额,这个数据是顶级对象 (First-Class Object)。在合约地址 0x1 中,还存储了一个智能合约代码 MyCoin,它是一个 ERC20 代币应用。而 MyCoin 这个代币的整个账本,都存储在 0x1 的空间中,怎么修改都由 0x1 中的合约代码说了算。

无论是有意还是无意,ERC20 代币非常容易出现安全漏洞。也就是说,在以太坊系统中,原生代币 ETH 和用户发行的代币并不享有同样的安全级别。

Libra 对 “资产” 编程

那么,能否不那么走极端,试图去抽象一些比简单数字更复杂的资产类型,而又不追求包罗万象的 “通用性” 呢?这正是 Libra 的出发点。Libra 可以定义类似一篮子货币、金融衍生品等比货币更复杂的资产类型,以及如何对他们进行操作,这种资产被称为 “资源”。Move 通过限制对资源的操作来防止不恰当的修改,从而提高资产的安全性。无论资源的操作逻辑如何,都必须满足两个约束条件:

  • 稀缺性。即资产总量必须受控,不允许用户随意复制资源。通俗的说,就是允许银行印钞,但不允许用户用复印机来 “制造” 新钱;
  • 权限控制。简单的说就是资源的操作必须满足某种预先定义的规则。例如,张三只能花自己的钱,而不允许花李四的钱。

Move的世界状态

上图是 Move 的世界状态,与以太坊不同,它把所有资产都当作是 “一等公民”(First-Class Resources),无论是 Libra 的原生代币,还是用户自己发行的资产。任何一个 “币种” 的余额,都存储在用户地址对应的空间中,对其进行操作受到严格的限制。这种被称为资源(resource)的对象,在交易中只能被移动,而且只能移动一次,既不能被复制,也不能被消毁。甚至严格到在代码中赋值给一个局部变量,而后面没有使用它也不允许。

这种资产的存储方式并非 Libra 独创,在此前的一些公链中已有应用,例如在 Vite 公链中,用户发行的币种余额也是顶级对象。不过 Move 可以支持更为复杂的资产类型,并对其提供额外的保护,这是 Libra 的主要贡献。

编者组: Vite 是本文作者创建的项目。

如何编程?

我们再来看看三个项目如何通过编程来实现丰富的扩展性。

比特币脚本

在比特币中,定义了一种 “比特币脚本”,用来描述花一笔钱的规则。比特币是基于 UTXO 模型的,只有满足了预先定义的脚本规则,才能花费一笔 UTXO。通过比特币脚本,可以实现 “多重签名” 之类的复杂逻辑。比特币脚本是一种非常简单的基于栈的字节码,不支持循环之类的复杂结构,也不是图灵完备的。虽然利用它可以在比特币网络上发行新的货币(Colored Coins),但它的描述能力非常有限,对开发者也不友好,无法应用到更复杂的场景中。

以太坊的 Solidity 语言

在以太坊中,定义了一种 Solidity 的编程语言,可以用来开发 “智能合约”。智能合约代码可以编译成一种基于栈的字节码 ——EVM Code,在以太坊虚拟机 EVM 中执行。Solidity 是一种高级语言,参考了 C++、Python 和 Javascript 的语法,是一种静态类型、图灵完备的语言,支持继承,允许用户自定义复杂的类型。Solidity 更像是一种通用的编程语言,理论上可以用来开发任何类型的程序,它没有针对货币或者资产类型的数据,在语法和语义上做任何限制和保护。比如用它来开发一个新的代币合约,代币的余额通常声明为 uint 类型,如果编码时对余额增减逻辑的处理不够小心,就会使余额变量发生溢出,造成超额铸币、随意增发、下溢增持等严重错误, 如: BEC 智能合约的漏洞

Libra 的 Move 语言

再来看 Libra,它定义了一种新的编程语言 Move,这种语言主要面向资产类数据,基于 Libra 所设定的 “顶级资源” 结构,主要设计目标是灵活性、安全性和可验证性。目前,Move 高级语言的语法设计还没有完成,白皮书只给出了 Move 的中间语言(Move IR)和 Move 字节码定义。因此我们无法评估最终 Move 语言对开发者是否友好,但从 Move IR 的设计中,可以感受到它在安全性和可验证性方面的特点。

Move 语言的设计

下面我们来简单介绍一下 Move 的语法。Move 的基本封装单元是 “模块”(Module),模块有点类似于以太坊中的 “智能合约”,或者面向对象语言中的 “类”。模块中可以定义 “资源”(Resource)和 “过程”(Procedure),类似于类中的 “成员”(Member) 和 “方法”(Method)。
所有部署在 Libra 上的模块都是全局的,通过类似于 Java 中的包名 + 类名的方式来引用,例如 0x001.MyModule,0x001 是一个 Libra 地址,MyModule 是一个模块名。模块中的过程有 public 和 private 两种可见性,公有过程可以被其他模块调用,私有过程只能被同模块的过程调用。而模块中的资源都是私有的,只有通过公有过程才能被其他模块访问。而且,外部模块或者过程对本模块资源的修改受到严格的限制,唯一允许的操作就是 “移动”(Move),不能随意对资源赋值。例如,Move 中是不允许出现一个类似于 MyCoin.setBalance() 这样的接口,让其他用户有机会随意修改某个币种余额的。

除了受限的资源类型,Move 模块中也允许定义非受限的成员,被称为非受限类型(Unrestricted Type),包括原生类型(boolean、uint64、address、bytes)和非资源类的结构体(struct)。这些非受限类型就没有那么严格的访问限制,可以用来描述与资产无关的其他应用类数据。从这个角度来说,Move 语言理论上应该具有和 Solidity 同样的描述能力,但由于实际的去中心化应用中,总会涉及到资产类的数据,而任何引用了资源类型的结构体也都是受限的,能够真正脱离 Move 语言严格限制的机会并不多。所以在实际使用 Move 语言开发的时候,程序员一定会有一种戴着镣铐跳舞的感觉,代码出现编译时和运行时失败的可能也更大。
通俗的说,用 Move 写代码不会让你感觉 “很爽”,这就是安全性和可验证性的代价。想想你用 C 语言自己控制内存的分配和释放时,虽然有一种 “我是上帝” 的感觉,但也会时刻忧虑缓冲区溢出、内存泄露等潜在风险;而用 Java 语言开发,虽然你不再能够为所欲为的控制内存,但也不用担心这些内存安全性问题了。自由还是安全,往往是不兼得的。

在一个 Libra 的交易(Transaction)中,也可以嵌入一段 Move 代码,被称为交易脚本(Transaction Script)。这段代码不属于任何模块,是一次性执行的,不能再被其他代码调用。脚本中可以包含多个过程,通过 main 过程作为入口来执行,在其中也可以调用其他模块中的过程。这个设计有点类似比特币,而和以太坊完全不同。在以太坊中,一个交易本身是不能包含一段可执行代码的,只能部署新合约或者调用一个已部署的合约。我不太喜欢 Libra 的这个设计,由于任何 Move 代码都必须经过字节码验证器(Bytecode Verifier)的严格检查才能发布到链上,这种一次性代码的边际成本远远高于可复用的模块,会拖慢交易被确认的速度,降低系统的吞吐量。交易脚本并不是必须的,大部分现实场景都可以通过模块来覆盖,而且,它的存在还增加了 Libra 钱包的开发和使用难度,有机会的话我会向 Libra 的开发团队提议取消这一设计。

通过一个交易脚本看 Move 语言

下面我来看一下白皮书中的示例代码片段,直观感受一下 Move 语言。请注意,这段代码是 Move 中间语言的(IR),未来 Move 高级语言肯定会提供一系列语法糖,使代码更加简洁优雅。

1
2
3
4
5
6
7
8
// 两个参数
public main(payee: address, amount: u64) {
// 从sender余额扣除amount个Coin
 let coin: 0x0.Currency.Coin = 0x0.Currency.withdraw_from_sender(copy(amount)); 
 
// 将coin累加到payee的Coin余额中
 0x0.Currency.deposit(copy(payee), move(coin));
}

这段代码是一个交易脚本,只有一个 main 过程,实现的是一个叫做 Coin 的代币转账逻辑,接受一个目标地址和转账金额作为参数,预期执行结果是把 amount 数量的 Coin,从交易发起者的账户转移给 address 地址。

过程体只有两行,第 2 行声明了一个 coin 变量,类型是 0x0.Currency.Coin。0x0 是部署 Currency 模块的 Libra 地址,Coin 是一个资源类型,属于 Currency 模块。这是一个赋值语句,coin 的值是调用 0x0.Currency 模块的 withdraw_from_sender () 过程获得的。这个过程被执行的时候,会从 sender 的余额中扣除 amount 数量的 Coin;

第 3 行调用 0x0.Currency 模块的另一个过程 deposit (),把上面取得的 coin 这个资源累加到 payee 地址的余额中。

这段代码的特别之处在于,每个取变量右值的地方都有一个 copy () 或者 move ()。这就是 Move 语言最有特点的地方,它借用了 C++ 11 和 Rust 的 move 语义,要求在读取变量的值时,必须指定取值的方式,要么是 copy,要么是 move

两种方式的差别是:用 copy 的方式取值,相当于把变量克隆出一份,原来的变量值不变,还可以继续使用;而用 move 的方式取值,原变量的引用,或者说所有权转移给了新的变量,原变量就失效了。

C++ 中引入 move 语义的目的,是为了减少不必要的对象拷贝,以及临时变量的构造和析构,提高代码执行效率;而 Move 语言的目的,是为了通过更严格的语法和语义限制,来提高 “资源” 变量的安全性。在 Move 语言中,资源类型只能 move,不能 copy,而且只能 move 一次。

假如程序员的咖啡喝完了,状态很差,在写这段代码时出了一个 bug,把第 3 行的 move (coin) 写成了 copy (coin),会发生什么呢?

1
2
3
4
5
public main(payee: address, amount: u64) {
  let coin: 0x0.Currency.Coin = 0x0.Currency.withdraw_from_sender(copy(amount));  
   // move(coin) -> copy(coin) 
  0x0.Currency.deposit(copy(payee), copy(coin));
}

由于 coin 是资源类型,不允许 copy,Move 的字节码验证器会在第 3 行报错。

再假如程序员写代码时,他的猫刚好从键盘上走过,踩到了 Command 和 D 键,于是,第 3 行代码重复出现了两次(第 4 行),又会发生什么呢?

1
2
3
4
5
public main(payee: address, amount: u64) {
  let coin: 0x0.Currency.Coin = 0x0.Currency.withdraw_from_sender(copy(amount));
  0x0.Currency.deposit(copy(payee), move(coin));
  0x0.Currency.deposit(copy(payee), move(coin)); // 猫干的!
}

这一次 bug 更严重,会导致来源地址只扣除了一次金额,而目标地址却增加了双倍的金额。在这个场景下 Move 的静态检查就真正发挥作用了,由于第一次 coin 变量经过 move 取值后已经不可用,那么第二次 move (coin) 就会引起字节码验证器报错。

在以太坊中就没有那么幸运了,比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity >=0.5.0 <0.7.0;
contract Coin {
   mapping (address => uint) public balances;
   event Sent(address from, address to, uint amount);

   function send(address receiver, uint amount) public {
       require(amount <= balances[msg.sender], "Insufficient balance.");
       balances[msg.sender] -= amount;
       balances[receiver] += amount;
       balances[receiver] += amount; // 又是猫干的!
       emit Sent(msg.sender, receiver, amount);
    } 
    // ………… 
}

以太坊是无法找到代码中多出来的一行 balances [receiver] += amount; 的(第 11 行), 每次 send () 被调用,Coin 这个代币的总量都会凭空多出 amount 个。

Move 字节码验证器

读到这里,大家应该能够意识到,Move 中最核心的组件就是字节码验证器。让我们来看看它是如何对一段 Move 字节码进行验证的,验证过程通常包括以下步骤:

  • 控制流图构建:这一步会将字节码分解成代码块,并构建它们之间的跳转关系;
  • 栈高度检查:这一步主要是防止栈的越界访问;
  • 类型检查:这一步会通过一个 “类型栈” 模型来对代码进行类型检查;
  • 资源检查:这一步主要针对资源类型进行安全性检查,防止资源被复制或消毁,并确保 – 资源变量被后续代码所使用。上文举的例子中的 bug,就是在这一步被发现的;
  • 引用检查:这一步参考了 Rust 的类型系统,对引用进行静态和动态检查。检查是在字节码级别进行的,确保没有悬空的引用(指向未分配内存的引用),以及引用的读写权限是安全的;
  • 全局状态链接:这一步主要检查结构体类型和过程的签名,确保模块的私有过程不会被调用,以及调用的参数列表符合过程的声明。

Move 虚拟机

Move 的虚拟机,和 EVM 相似的地方比较多。它也是一个基于栈的虚拟机。指令集包含 6 类指令:数据加载和移动、栈操作 / 代数运算 / 逻辑运算、模块成员及资源操作、引用相关操作、控制流操作、区块链相关操作。

与 EVM 类似,每一条指令都会计算一个 gas,耗光 gas 后代码会停机。Move 中,一个交易的代码执行符合原子性,要么全部执行成功,要么一条也不执行。有趣的是,虽然 Libra 是一个标准的区块链账本结构,所有交易都是全局有序的,但 Move 语言本身支持并行执行,这意味着,也许以后 Libra 可以改进成类似 Vite 的 DAG 账本,提高交易并行处理的效率。

未来工作

当前 Move 还处于一个比较早起的开发阶段,后续工作包括:

  • 实现 Libra 链的基本功能,包括账户、Libra 代币、准备金管理、验证节点的加入和移除、交易手续费管理、冷钱包等;
  • 新的语言功能,包括范型、容器、事件、合约升级等;
  • 提高开发者体验,包括设计一个人性化的高级语言等;
  • 形式化建模和验证工具;
  • 支持第三方 Move 模块。

本文如有错误,请读者不吝指正。想获取更多的细节,可以阅读白皮书或开源代码。 顺便说一句,这篇 Move 白皮书写的相当不错,概念准确,而且通俗易懂,没有使用特别形式化的描述或者复杂的数学知识,一个对区块链技术有所了解的读者完全可以一次读懂。这也从侧面反映出 Facebook 团队专业和务实的风格。

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享