CKB 上租赁管理的实现思路

原文:https://www.driftluo.com/article/87b4887d-54d9-478c-ac3c-d70271d409f6

ckb 主网上线快两月了,就目前看,链上开发还存在一定的难度,有很多概念和定义并没有出现在文档里面,更多的是代码层面的定义。这篇文章主要的目的是以实现租赁管理为手段来介绍 ckb 链上编程,涉及实际代码的地方会很少,更多的是思路的讲解,希望对开发者有一定的启发。

从 ckb verify 讲起

ckb 的 Transaction 是由一堆 cell 构成的,大致上可以分为引用的 cell 和拥有所有权的 cell,而每一个 Transaction 的作用归结到最后只是所有权的变更,它可能是所有权转移也可能只是更改 cell 自身的状态(考虑交易费的存在,这类交易已经不可能存在了),也就是说,每一笔交易必然存在所有权的转移,哪怕存在方式是隐式的交易费。

ckb 的编程说到底还是链上状态的变更,普通转账是简单的所有权变更,Dao 是 Cell 状态的变更,将来的 UDT 或者其他合约也是如此。无论什么样的变幻,都需要一个验证机制去保证交易产生的状态变更符合发起人的预期,而这样的 Transaction 验证(verify)就是整个链上编程的核心,也是最后的保护。开发者首先需要了解 verify 是怎么工作的,之后才能更好地写出匹配验证的逻辑和业务。

table RawTransaction {
    version:        Uint32,
    cell_deps:      CellDepVec,   // 依赖的 cell
    header_deps:    Byte32Vec,  // 依赖的 header
    inputs:         CellInputVec,  // 需要销毁的 cell 列表
    outputs:        CellOutputVec,  // 生成的 cell 列表
    outputs_data:   BytesVec,  // 生成 cell 列表中每个 cell 对应的 data 数据
}

table Transaction {
    raw:            RawTransaction,
    witnesses:      BytesVec,  // 见证信息
}

上方是 Transaction 的字段信息,大部分已经标注了字段的含义,接下来,大致讲解一下 ckb 中对 cell 的状态变化是怎么验证的。我们都知道每一个 cell 都可以同时有 lockscript 和可选的 typescript 两种 script 作为验证脚本,而一个 cell 被消耗时,都需要跑一遍它的 lockscript 和可能存在的 typescript,每一个 cell 被创建时,都需要跑一遍它的 typescript:

cell consume:

if run(cell.lock_script) == 0 && run(cell.type_script) == 0:
    return success

cell create:

if run(cell.type_script) == 0:
    return success

上面这段伪代码就是 cell 验证的核心了,而之后又因为工程实现和便利问题有许多扩展:

  • cell_deps 是脚本执行的上下文中需要的必要环境,诸如加密库依赖、hash 库依赖
  • header_dep 获取链上时间的手段
  • dep_group 依赖库过于庞大,需要拆分 cell 进行存储,并在运行时统一加载
  • type_id 保留依赖库能更新但不 break 生态的手段(可查看第 0 个块的第二个 output 的 type script)
  • scriptgroup 将参数和脚本相同的 script 合并为同一组执行,减少 cycle 消耗

简单讲一下 header_dep,这是一个妥协的结果,在区块链上,真实时间是无法获取的:

  • 执行环境是禁止访问宿主机的虚拟环境
  • block 记录的 time 是不准确的,它的验证是一个阈值范围
  • 新 block 上记录的 timestamp、epoch 都不可信,无法自证

于是需要用链上历史证明时间。

脚本执行可以看出是一个函数的执行过程,它需要入参,而入参来源可以分为两个,一是临时参数,即一次性使用,二是从链上获取的参数,可以把 Transaction 的 witness 认为是临时参数,而链上任意 cell 中 data 的数据(vm 提供了很多 syscall 接口用来获取链上数据)认为是链上参数。

基于上述概念,我们需要知道的基础逻辑是:

  • 任何 Transaction 本质是 cell 状态的变更
  • cell 的变幻可以用 script 进行验证

租赁

这个业务在短期内是不会有市场的,当它正式登录 ckb 的时候,应用市场应该已经非常繁荣了。

我们说的租赁是什么概念呢?

首先,任何 cell 都是有所有权的,它可能是以任意方式存在于 lockscript 中,只有符合需求的参数输入才能解锁和使用对应的 cell,例如默认实现的单签和多签脚本

其次,cell 中的 data 字段是可以存储任意数据的,包括各种需要的算法或者库实现,并且可以在任意交易验证逻辑中加载使用

最后,当自身拥有的 ckb 不足以承载想要部署的数据时,可以有两种办法解决,第一是买,第二是租赁,但是租赁并没有默认提供的实现,接下来,就来谈谈实现它的思路

畅想使用方式

在考虑如何实现之前,需要想清楚用户应该如何使用它,只有使用起来简单才有可能真正用起来。那么对于租赁的使用,我大致想了以下几个方面的需求:

  • 租赁方无需与出租方沟通即可自行完成租赁过程
  • 租赁方可自行调整租赁时间
  • 出租方可自行将拥有的 ckb 转换为待出租的状态
  • 出租方可自定义租赁 epoch 单价、租赁 epoch 上限等
  • 租赁期间内,租赁方可进行 cell 状态变更(Option)
  • 租赁期间内,出租方无权强行收回 cell 空间

实现思路

整个实现思路是用组合的方式完成一个一个需求,而实际上最后的实现可以是多种多样的,复杂程度也并不相同,仅供参考。

租赁方无需与出租方沟通即可自行完成租赁过程是一个什么概念呢,这意味着任何人都可以使用该 cell ,也就是说,放弃 lockscript 的销毁验证,转为 typescript 生成验证,typescript 验证的逻辑是,一,确认当前 cell 的状态,二,确认生成 cell 的 type 和 lock 符合预期。而任何人都可以租赁的前提是,首先部署一个 cell 作为 always success lock ,这样,待租赁的 cell 大概和下面差不多:

cell {
    capacity: 待租赁大小
    lock: always success lock
    type:租赁合约
    data:{
        max_term_of_lease(unit: epoch): u32,
        unit_price(ckb/epoch): u64
        lock_args: 出租方 lock args
        code_hash: 
    }
}

任何人只要将其拥有的 ckb 转成上诉格式,即代表进入待租赁状态,而此时,可能需要一个第三方服务去展示链上目前可租赁的 cell 的信息,这大概是唯一需要链外做的事情了。

下面方案中,验证租赁时间是否大于规定最大时间等基本验证被忽略了,留下的是关键的所有权转移的验证方案。

第一种方案

lockscript 用默认实现的带 since 约束的 lock_args,只需要写一个通用的 typescript,同时 typescript 的逻辑也相对来说比较简单,这种方案租赁过程是一次性的,租赁人无法在租赁期间内更改租赁 cell 的状态:

// 待租赁 cell 初始状态
if input_cell.lock == always_success_lock_hash:
    // cell 所有人退出租赁模式
    if output_cell.lock_args == input_cell.data.lock_args && output_cell.lock.code_hash == input_cell.data.lock.code_hash && output_cell.capacity == input_cell.capacity:
        return success
    // 租赁人付费用并使用
    elif output_cell.lock_args == input_cell.data.lock_args + time_since && output_cell.lock.code_hash == input_cell.data.code_hash && output_cell.capacity >= input_cell.capacity + witness.term_of_lease_epoch_num * input_cell.data.unit_price:
        return success
elif:
    // 租赁中 或 转入租赁状态
    return success

第二种方案

在第一种方案的基础上,需要实现租赁期间内的租赁人可变更状态的同时,限制出租人只有在租赁结束后才可以进行状态变更。这里复杂的地方是 type 和 lock 都需要做一定的修改。

lock 实现 or 模式:

if lock_arg == 租赁人 or lock_arg + since == 出租人 + since:
    return success

type 实现会复杂一点:

// 待租赁 cell 初始状态
if input_cell.lock == always_success_lock_hash:
    // cell 所有人退出租赁模式
    if output_cell.lock_args == input_cell.data.lock_args && output_cell.lock_hash == input_cell.data.lock_hash && output_cell.capacity == input_cell.capacity:
        return success
    // 租赁人付费用并使用
    elif output_cell.lock_args == any_lock_args_by_lessee + input_cell.data.lock_args + time_since && output_cell.lock.code_hash == or_code_hash  && output_cell.capacity >= input_cell.capacity + witness.term_of_lease_epoch_num * input_cell.data.unit_price && output_cell.data.lock_hash == input_cell.data.lock_hash:
        return success
elif  input_cell.lock.code_hash == or_code_hash:
    // 租赁中租赁人变更 cell 状态
    if output_cell.lock == input_cell.lock && output_cell.lock_hash == input_cell.lock_hash && output_cell.capacity >= input_cell.capacity && output_cell_type == input_cell_type && output_cell.data.lock_hash == input_cell.data.lock_hash:
        return success
    // 出租人取回到期的 cell
    elif output_cell.lock_arg == input_cell.data.lock_arg && output_cell.lock.code_hash == input_cell.data.code_hash:
        return success
elif:
    // 转入租赁状态
    return success

注意到,因为本方案 租赁人在租赁期间内可以无限度地更改 cell,我们需要对租赁状态下 cell 的 data 字段做一个约定,这个约定将保证任何变化都不会影响到 cell 所有权的判断:

output_cell.lock_arg == input_cell.data.lock_arg && output_cell.lock.code_hash == input_cell.data.code_hash

即 cell 的 data 必须保留所有权的 lock_hash,并且一直跟随到租赁结束,如果不在 cell 中保留所有者的 lock 信息,那多次转移后,所有权将难以在一个 Transaction 中进行追溯,可能需要类似 Dao 合约的 header_dep 的形式进行追踪,耗费的 cycle 与直接保存数据存在巨大差异。

小结

以上两个方案可以看出很明显的思路倾向:实际上 typescript 里就是写一个状态机,确认当前状态,下一步可到达的状态,然后验证 input 和 output 是否符合状态机允许的状态转移流程,这与写编译器前端的词法语法解析类似。当然,我不排除有其他的合约思路。

更多的可能性

上诉的思路只是众多方案中的一小部分,更多的可能方案也许会比上面的思路更加出色,本文的重点并不是实现一个租赁合约,而是通过租赁的可能实现思路来理解 ckb 上 cell 编程的使用方法,期待更多更好的合约和产品的出现。

10 Likes

在 Data 里面写入出租信息的一个潜在问题是,承租人拿到 cell 后不方便使用,比如用 cell 保存一段 binary 用来运行 dapp。是否把出租信息写入 lock 更好?

这个主要是看实现时的取舍问题了,无论放哪,放 data 的问题是每次使用都需要人为对数据进行截断处理,好在这部分数据都是定长的,问题应该仅限于麻烦,而放在 lock 里的话,验证逻辑就需要从 lock 中获取足够的信息,在我看来其实对 typescript 的定义并没有很大的变化,主要是方便 data 的数据引用了

通过 godwoken 支持 EIP4907 也能实现租赁
差异在哪里呢?