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