Background
目前,CKB 上的 Script 通常作为 Verifier 使用。Script 只允许满足规则的交易通过,以此控制 Cell 的状态转移。在这种范式下,Script 本身几乎不对外暴露任何逻辑,Script 的更多信息只能在链下或链上弱绑定的方式约定。
随着 CKB 上的应用方越来越多,问题逐渐浮出水面。这包括 UDT 类资产如何声明自己的代币信息,NFT 类资产如何定义自己的数据结构,怎样解决交易组装的逻辑需要用不同语言重复实现等。我们迫切需要 Script 强绑定的信息/逻辑来解决应用间的互操作性问题。
这时,DOB 协议在链下执行 Decoder 的模式为我们揭示了一条简单有效的路径。
Introduction
来源于 Script 的富信息(Script-Sourced Rich Information, SSRI)方案是对 Script 能力的一次扩展。作为最显著的能力,Script 能在链下执行并返回结果,让开发者可以将任意的信息/逻辑嵌入到 Script 中,向链下应用描述自己的行为和管理的数据。
Design
SSRI 方案由两部分构成:
- SSRI 在链上定义了一套 Script 的行为标准。类似于 Rust 中的
trait
,SSRI 让我们得以将行为抽象为一系列的method
约束,满足约束的 Script 可以被认为具有特定的行为。调用方可以通过指定的方法与 Script 进行交互。 - SSRI 在链下定义了一套 Script 执行的标准。在链下执行的 Script 可以通过特定的 syscall 获得更多的信息来源来构建输出。
Methods
Method Path
在 SSRI 的通用行为标准中,调用方通过被称为方法路径(Method path)的 64 位(8 字节)指针, 来调用 Script 具体的一个方法。
我们约定,方法路径为方法签名 CKB hash 后的前 8 字节。方法签名的格式为 [<trait>.]<method>
,如 calc_unlock_time
或 UDT.symbol
。我们强烈建议通用的抽象行为方法都应该带有 trait
前缀以避免冲突。因为方法类型不影响方法路径,所以和 Rust 一样,Script 方法不支持重载。
Distribution
为了实现通过方法路径来控制 Script 的行为,我们注意到 CKB VM 的程序入口定义如下:
fn __ckb_std_main(argc: core::ffi::c_int, argv: *const $crate::env::Arg) -> i8;
和 C 的程序入口一样, argc
和 argv
共同描述了程序的参数。
CKB Script 的 Rust 包
ckb-std
提供了ckb_std
::
env
::
argv
方法来访问这些参数。
我们约定,遵守 SSRI 的 Script 应当从 argv[0]
读取方法路径,并且将剩余的参数传递给方法。开发者可以自行决定未定义情况的处理方式,包括未指定方法路径/方法路径长度错误/方法不存在。我们推荐:
- 若未指定方法路径,Script 可以按原来的 Verifier 逻辑执行。
- 若方法路径长度错误或方法不存在,Script 应当以任意错误码退出。
Off-Chain Syscalls
链下 Script 依然在 CKB VM 中执行,此处列举了目前链下可用的 Syscall 及他们与链上版本的区别,详细的介绍见 VM Syscalls,VM Syscalls 2 和 VM Syscalls 3。
在链下执行的 Script 可以不依附在任何实体上,这意味着 Script 不一定总能调用全部 syscall。我们将执行环境分为四个等级:
Code
。只有程序代码。Script
。完整的 Script 结构。Cell
。完整的 Cell 结构。Transaction
。完整的 Transaction 结构。
VM Version - Code
目前,如果一个 Script 在链下被执行,VM version syscall 应当返回 -1。
Exit - Code
Set Content - Code
最大输出长度受执行环境决定,不小于 256K。
Debug - Code
Load Script Hash - Script
Load Script - Script
Load Cell - Cell
如果执行等级为 Cell
,index
必须为 0,source
必须为 Source::InputGroup
。
Load Cell By Field - Cell
如果执行等级为 Cell
,index
必须为 0,source
必须为 Source::InputGroup
。
Load Cell Data - Cell
如果执行等级为 Cell
,index
必须为 0,source
必须为 Source::InputGroup
。
Find Out Point By Type - Code
int ckb_find_out_point_by_type(void* addr, size_t* len,
const void* type_script, size_t script_len)
{
return syscall(u64_le_from_bytes(method_path("find_out_point_by_type")), addr, len, type_script, script_len, 0, 0);
}
Script 在链下执行时特有的 Syscall。该 Syscall 会以 type script 在链上搜索一个 Live cell,若存在则将第一个找到的 Cell(注意,这意味着结果可能不总是固定的)对应的 Out point 存入到 addr 中,len
为 36。否则 len
为 0。
Find Cell By Out Point - Code
int ckb_find_cell_by_out_point(void* addr, size_t* len, const void* out_point)
{
return syscall(u64_le_from_bytes(method_path("find_cell_by_out_point")), addr, len, out_point, 0, 0, 0);
}
Script 在链下执行时特有的 Syscall。该 Syscall 会以 Out point 在链上搜索一个 Live cell,若存在则将其 Cell output 存入到 addr
中。否则 len
为 0。
Find Cell Data By Out Point - Code
int ckb_find_cell_data_by_out_point(void* addr, size_t* len, const void* out_point)
{
return syscall(u64_le_from_bytes(method_path("find_cell_data_by_out_point")), addr, len, out_point, 0, 0, 0);
}
Script 在链下执行时特有的 Syscall。该 Syscall 会以 Out point 在链上搜索一个 Live cell,若存在则将其 Cell data 存入到 addr
中。否则 len
为 0。
SSRI Trait
所有使用了 SSRI 方案的 Script 都应当实现这些方法,以帮助 Script 被使用。在使用了 SSRI 方案的 Trait 设计中,我们默认使用 Rust 的类型描述参数和返回值:
- 数字类型均用小端序编码。
- 布尔类型占用单个字节,若为 0 则为 false,否则为 true。
- 字符串类型以 utf-8 格式编码,按 Molecule 中的
vector<byte>
类型储存。 - 复合类型使用 Molecule 编码。
SSRI.version
- Code
fn version() -> u8;
目前,该方法总应返回 0。
SSRI.get_methods
- Code
fn get_methods(offset: u64, limit: u64) -> Vec<Bytes8>;
用于枚举 Script 所有的方法。输入参数 offset
和 limit
用作分页,limit
若为 0 则不限制,返回对应的方法路径数组。
SSRI.has_methods
- Code
fn has_methods(methods: Vec<Bytes8>) -> Vec<bool>;
判断一组方法是否存在。参数为目标方法路径数组。
SSRI.get_cell_deps
- Code
fn get_cell_deps(offset: u64, limit: u64) -> Vec<CellDep>;
用于枚举 Script 在链上执行时需要的 cell deps (不包含自己)。输入参数 offset
和 limit
用作分页,limit
若为 0 则不限制,返回对应的 cell deps 数组。
SSRI - UDT
基于 SSRI 方案,我们可以描述 UDT 类型的 Script 应当具有的行为。