1. 引言

前序博客有:

  • zkMove——针对Move合约生态的zkVM


定位为高性能L1的Aptos和Sui,均采用Move合约编程语言。Solana也定位为高性能L1,但其采用Rust合约编程语言。本文重点对比Sui/Move和Solana/Rust合约编程语言。【Aptos/Move为不同的Move变种,有细微的差别。不过只要原生支持Move bytecode,则所有主要Move优势适于所有Move变种。】

Solana中的智能合约为programs,Move中的智能合约为modules。

2. Solana编程模型

Solana的智能合约编程模型是基于programs和accounts。Solana中,通常将智能合约称为“programs”。

  • 1)programs(智能合约):定义交易中调用该program的处理逻辑。可在program调用中传入任意参数。program本身是stateless的,无法维护、读写任意状态。将program+account结合,可维护、读写状态。program调用由clients发起(通常是由web apps发起,也可自己手动发起直接调用)。
  • 2)accounts:主要用于存储program state。programs会读写accounts,相应的数据可跨交易留存。每个account由其address(即为某Ed25519密钥对的公钥)唯一标识。accounts具有指定大小,且可存储任意数据。
    此外,还有与每个account相关联的metadata,其中包含有关account的重要信息。program本身(可执行文件)也存储在account中,并具有address(称为program ID)。没有固有的结构来解释数据——从运行时的角度来看,它只是一个指定大小的字节数组。
    可将Solana的account space看成是a global key-value store,其中key为account address,value为account data。program会对该key-value store进行读写。
    • account会维护balance和data,其中data为指定长度的byte数组。
    • account具有“owner” field,owner为the Public Key of the program that governs the state transitions for the account。

Solana中的program是无状态的,需依赖account中的data数组来实现state transition:

  • 1)Programs can only change the data of accounts they own.
  • 2)Programs can only debit accounts they own.
  • 3)Any program can credit any account.
  • 4)Any program can read any account.

program分为:

  • System Program
  • User-defined Program:名为“Loader Program”的System program会加载user-defined program,并将相应account中的data标记为executable。

默认情况下,所有的accounts初始owner都为System program:

  • 1)System Program is the only program that can assign account ownership.
  • 2)System Program is the only program that can allocate zero-initialized data.
  • 3)Assignment of account ownership can only occur once in the lifetime of an account.

注意:Solana中,client在发起program call时,需提前指定该调用中所访问的accounts,并确定是读还是读写相应的account。这对交易处理性能来说至关重要。由于runtime现在知道了每笔交易总哪些accounts会被modify,可规划可并行执行的non-overlapping transactions,从而保证数据一致性。这也是Solana具有高吞吐量的关键原因之一。所谓的“EVM doesn’t scale”,是指EVM无法对交易进行并行处理。更多关于Solana交易处理runtime可参看Anatoly Yakovenko 2019年博客Sealevel — Parallel Processing Thousands of Smart Contracts。

可将Solana合约看成是操作系统中的程序,accounts看成是文件,任何人都可自由部署和运行任何合约,当合约运行时,可读写文件(accounts)。文件对所有合约都是可读的,但仅具有某文件ownership的合约才有权限对该文件进行写操作。合约之间不信任但可相互调用,在实际执行时,都需要假设输入具有潜在恶意。由于操作系统可供任何人全局访问,会为合约提供原生的验签支持,为用户提供authority和ownership functionality。

2.1 Solana安全模型

2.1.1 account ownership

Solana的account具有ownership的概念。 每个account严格由且仅由一个program拥有。 即意味着只有拥有该account的program才可对其进行修改。其它program无法对该account修改。account ownership信息存储在account的metadata中,且无法由user programs修改。

2.1.2 account signatures

client在发起program call时,可附加account签名(注意每个account关联一组Ed25519密钥对)。当有某account的私钥时,可用其对交易签名,然后在program runtime中会将该account标记为“signer”,program可利用该信息实现ownership and authority functionality。事实上,这也是Solana钱包的实现原理。每个account关联有一定数量的SOL(也存储在account metadata中),当需要转账时,需使用account的私钥对转账交易进行签名。因此,所谓的Solana钱包,是指你拥有相应私钥的account。

2.1.3 CPI calls

也通过CPI(cross-program invocation)接口实现跨合约调用——由合约A调用合约B。CPI calls与client program calls非常类似,以program ID来表示想要调用的program,并传入相应的参数和所需的accounts。内部,CPI calls通过syscall——runtime将以 与client直接调用 相同的方式执行指定的program。

2.1.4 PDA accounts

program本身在CPI calls中也具备提供account signatures的能力——通过PDA(program derived address)accounts来实现。
PDA accounts是特殊类型的accounts,仅可由program来签名。【program可不拥有或存储相应的私钥】
PDA accounts为program specific的,每个program可生成其想要的任意数量的PDA accounts。用户 或 其它program,无法为特定program的PDA accounts 提供签名。
PDA accounts的重要性在于,program具备了authority和ownership能力。如,program可own tokens,为“某token vaults仅由某program具有取款功能,而任何user或其它program均无相应权限” 的原理——该vault由program logic完全保护——即,若program是正确的,则vault是安全的。
PDA 还可用于创建具有指定address的accounts。

2.2 Solana智能合约Rust编程

  • 1)accounts are owned by programs which gives them the sole permission to mutate them
  • 2)you can sign a transaction with the account’s private key and the runtime will mark this account as a “signer” that the program implementation defines the semantics of
  • 3)the programs can call each other using CPI calls
  • 4)PDAs allow programs to also provide account signatures

以上4点为Solana智能合约安全性和可组合型的基石。

只要能编译为SBF(Solana Bytecode Format,改良版的eBPF),Solana合约可 以任意语言编写,通常,会选择Rust语言。

Solana的所有合约都会定义如下的entrypoint:

当合约被调用时,runtime会调用相应的entrypoint函数,传入的参数有:

  • program_id:为所调用的合约的program ID(存储该合约的account address)——因有时(非常罕见)利用该参数可将相同的合约部署在不同的地址(program ID)。
  • accounts:由client指定。为AccountInfo结构体类型,包含了account data以及相关的metadata(如哪个合约是其owner、该account是否可mutated、交易是否已被其私钥签名等等)。
  • instruction_data:由client指定,通常为参数的编码。

合约会根据以上参数进行处理并执行相应的逻辑。

注意:Solana中每笔交易可包含多个sequential program calls(对同一或不同合约)——被称为“instructions”。交易是atomic的,若instructions(program calls)中有任何fail,则整笔交易将fail,对全局状态无任何影响。

2.2.1 Input Safety

由于client可传入任意accounts和任意instruction data,合约必须仔细处理这些输入,确保恶意编写的输入不会以意外的方式影响程序执行。

2.2.2 Instructions

大多数时候,都希望在同一合约内实现多个不同的instruction calls——如Token Program会实现InitializeAccount、IntializeMint、Transfer等多个接口,每个接口具有不同的参数。因此,通常instruction_data的第一个字节用来标识所调用的instruction(最多支持256个不同的接口),后面的字节为对相应参数的编码。

2.3 Solana智能合约Anchor编程

Solana合约的粗略流程为:

  • 1)定义entrypoint函数
  • 2)对instruction call及其参数进行解码
  • 3)调用相应的instruction handler
  • 4)做account和参数检查(至关重要,确保输入未被污染)
  • 5)处理instruction,并根据需要解码account data

Rust语言并为提供相应的工具来对以上某些流程进行streamline处理。而Anchor可以。

Anchor为用Rust语言编写的Solana合约开发框架——填补了raw Rust的gaps,并提供了安全性,使得Solana合约开发更人体工学、更安全。

3. Sui/Move编程模型

在Move中,合约发布为modules(模块)。modules中包含:

  • functions:函数可调用其他函数(同一模块内,或其它模块的public函数)。
  • custom types(structs):structs中可包含u8\u64\bool等类型 或 其它structs。

具有的主要优势为:

  • global type safety
  • global resource safety
  • memory safety
  • embeddable objects等

3.1 Sui/Move的objects

Sui/Move变种与Aptos或Diem等变种不同之处在于,其引入了objects的概念。
Objects为runtime和跨交易persist state 所存储的struct instances。
Sui有3种不同类型的objects:

  • 1)owned objects:归user所有。仅拥有该object的user才可在交易中使用owned objects。相应的所有权metadata是透明的,并由runtime处理。其实现原理为公钥密码学技术,runtime中object的metadata中存储了相应的公钥,当在交易中需使用该object时,需提供相应的签名(当前支持Ed25519签名,即将支持ECDSA和K-of-N多签)。
  • 2)shared objects:与owned objects类似,但是无关联的owner。因此,无需拥有相应私钥即可在交易中使用shared objects。任何owned objects,都可由其owner分享设置为shared objects。而一旦设置分享后,该object将永远为shared objects,无法再转换为owned objects。
  • 3)immutable objects:为不可修改的objects。一旦某object被标记为immutable,该object的fields将不可修改。与shared objects类似,immutable objects也没有owner,且可被任何人使用。

整个Sui/Move编程模型非常直观和简单。每个合约都是一个模块,包含了函数和结构体定义。
结构体:

  • 由函数实例化
  • 可通过函数调用传递给其它模块

为实现跨交易的结构体留存,需将结构体转换为object(可为owned、shared或immutable类型)。

3.2 Sui/Move的安全性

Sui/Move中:

  • 可将你的owned objects(或shared objects)传输给任何模块的任何函数。
  • 任何人都可发布模块,模块可能是潜在恶意的。
  • 没有模块粒度的owned structs,即不存在所谓的owner module来唯一改变某struct。structs可流入其它模块,并嵌入在其它structs中。

那么问题来了,如何保证其安全性呢?当某人发布了一个恶意模块,获取了某shared object(类似某AMM pool)并将该object发送到恶意模块中试图drain of its funds,如何解决这种情况呢?
在Solana中,引入了account ownership的概念,仅拥有该account的program才可修改该account。但在Move中,不存在模块粒度的owned objects,可将objects(不只包含object reference,还包含整个object本身的值)发送给任意模块。因此,runtime无法做特定的检查来确保该object不会被不信任的模块非法修改。如何来保证object的安全性呢?

3.2.1 Move中的structs

传统的Rust结构体定义为:

struct Foo {x: u64,y: bool}

Move中的structs有如下约束:

  • 1)仅定义struct的模块可对其进行实例化(“packed”)和销毁(“unpacked”)。即,无法在其它模块的任何函数内对该struct进行实例化或销毁。
  • 2)struct实例中的fields,仅可由其所属的模块进行访问修改。
  • 3)无法在其它模块中对该struct实例进行复制。
  • 4)无法将某struct实例存储在其它struct实例的某field中。

即意味着,在其它模块的某函数内,对该struct实例:

  • 无法修改该结构体实例的fields(key)
  • 无法复制该结构体实例(copy)
  • 无法将该结构体实例存储在其它结构体的某field内(store)
  • 无法销毁该结构体实例(drop)

这些限制会失去一些灵活性,但智能合约开发的本质是对数字资产(resources)进行编程。
为此,Move中的结构体定义可附加相应的capabilities:key、store、copy、drop,类似为:

struct Foo has key, store, copy, drop {id: UID,x: u64,y: bool}
  • 1)key:表示该结构体为object。object可留存,若为owned object,在合约调用时需提供相应的签名。当使用了key能力时,该结构体中的第一个元素必须为object ID,类型为UID。从而具有全局唯一ID,便于引用。
  • 2)store:表示允许将该结构体到其它结构体的某field内。
  • 3)copy:表示允许在其它模块中对该结构体进行任意复制。
  • 4)drop:表示允许在其它模块中对该结构体进行销毁。

也就是说,Move中的struct默认为resource,capabilities给了相应的权限来放松以上限制,使得其功能更像传统的结构体。

3.2.2 Move中的bytecode verification

Move合约以模块形式发布,任何人都可创建并上传任意模块到链上供任何人执行。如何保证模块遵循相应的规则呢?
Move verifier为静态分享工具,可分析Move bytecode,并确定其是否遵循了相应的类型、内存、resoucre安全准则。所有上传到链上的代码均需经该verifier验证通过。

在将某Move模块上传到链上时,节点和validators会先运行verifier,只有验证通过后才会将该模块commit。若该模块未遵循Move安全准则,则会被verifier拒绝,不会上传到链上。

Move bytecode 和 verifier 是Move的核心创新。正是它使以resource为中心的直观编程模型成为可能,否则这是不可能的。关键的是,它允许结构化类型跨信任边界传递,而不会丢失其完整性。

4. Sui/Move vs. Solana/Rust

Solana中的智能合约为programs,Move中的智能合约为modules。
二者的主要区别在于:

  • Solana的跨program边界不具备type safety。每个program手工从raw account data中解码加载instances,要求需要手工进行严格的安全检查。
  • Solana中无原生的resource safety。Solana中的resource safety由各个合约独自实现。这提供了足够的可编程性,但却牺牲了可组合性以及人体工学。Move原生支持resource safety,可安全地在untrusted code中流入流出。
  • Move中,the types do exist across modules,即type system是全局的。即意味着无需CPI call,无需account编解码、无需account ownership检查等待。在调用其他模块的函数时,仅需要直接传参即可。
    Move中的type safety和resource safety由compile/publish时的bytecode verification保证,不像Solana需在合约层中实现,并在runtime时做相应检查。

5. Move on Solana

如何让Solana支持Move呢?
可能的实现方式有3种:

  • 1)添加Move MV,将其与SBF VM一起,作为native loader:在runtime中额外增加Move loader。Move合约存储为链上Move bytecode并由Move VM执行(与Sui类似),但这意味着Solana中同时又SBF合约生态和Move合约生态。但:

    • 1.1)难点在于这2个生态直接的合约调用。
    • 1.2)开发难度大,需同时深入了解Move和Solana,Verifier也可能需要调整。
    • 1.3)runtime中维护两个不同的loader难度大。bug风险加倍,任一loader中的bug,都会给整条链带来风险。

    详细讨论见:

    • 2019年 Embed Move #5150
    • 2020年 Remove move_loader and librapay #11184
    • 2022年 审计风险安全讨论
  • 2)以program来运行Move VM(类似Neon):以Solana合约来运行Move VM。类似Neon以Solana合约来运行EVM。

  • 3)将Move编译为SBF(类似Solang):需为Move构建一个LLVM frontend。这样runtime就无需感知Move语言。

    • 3.1)从runtime的角度来说这是最优方案,runtime及其安全假设均无需修改。
    • 3.2)从合约角度来说,Move的一些语法Solana不支持,如Move的global type safety、global resource safety、embeddable objects等优势,可能均需替换为accounts checks、CPI calls、PDAs等等。

Move的最大优势在于其bytecode verification:
The distinguishing feature of Move is an executable bytecode representation with resource safety guarantees for all programs. This is crucially important given the open deployment model for contracts — recall that any contract must tolerate arbitrary interactions with untrusted code. Source-level linearity has limited value if it can be violated by untrusted code at the executable level (e.g., untrusted code that duplicates a source-level linear type).

参考资料

[1] Klas 2022年9月博客 Smart Contract Development — Move vs. Rust
[2] 2022年9月 twitter Smart Contract Development — Move vs. Rust
[3] Klas 2022年7月博客 Solana for Non Smart Contract Developers