# CKB ACL Research Report
## Metadata
**Status**:: #x
**Zettel**:: #zettel/permanent
**Created**:: [[2023-10-19]]
**Tags**:: #work
%%
**Notion**:: [CKB ACL Research Report](https://www.notion.so/cryptape/CKB-ACL-Research-Report-ef2877d8c2114668bb1a6e9a74cacc6c)
%%
## Introduction
UTXO 模型在模块组合方面缺少一个广泛认同的模式,导致很难通过模块封装和组合的方式来降低开发门槛。我们一直在探索希望能找到一个易懂易用的模块组合方案,最新的进展可以查看 [A New Quest on Composable CKB Dapp Design](https://www.notion.so/cryptape/A-New-Quest-on-Composable-CKB-Dapp-Design-4fe7ff84744a4941a5ccebef394c422f)。本报告以模块间授权的视角出发,提出一套新的抽象结构进行模块组合。该方案被验证能满足游戏 ECS 风格的模块划分,并能解决一些常见的应用间协作的问题。这一方案在 CKB2023 生效前即可使用,在生效后则会更加简便。
## Background
### ACL
**访问控制列表**(**ACL**)[^1] 是一份与资源、对象或设施相关的权限列表。ACL指定了哪些用户或程序被授予访问资源的权限,以及对给定资源允许进行哪些操作。一个典型的ACL中的每个条目都指定了一个主体和一个操作。例如,如果一个文件对象的ACL包含(Alice:读,写;Bob:读),这将允许Alice读取和写入该文件,并仅允许Bob读取该文件。
[^1]: [Access-control list - Wikipedia](https://en.wikipedia.org/wiki/Access-control_list)
### CKB Script
在 CKB 中,Script 是依附于 Cell 上的可执行程序。当 Cell 被包含于一个交易中时,这些可执行程序将被执行以验证交易是否有效。
Script 可以被简化成一个函数:`tx -> bool`,即输入一个交易返回真或者假。
每个 Cell 可以设置两个 Script,Lock Script 和 Type Script。一般来说,Lock Script 用于授权交易使用相应的 Cells,而 Type Script 则用于验证交易逻辑的正确性。在最新的 [Witness 格式设计][witness-otx-typed-message] 中也延续了这一模式。
[witness-otx-typed-message]: https://www.notion.so/cryptape/Witness-OTX-Typed-Message-c6f11cb982474d77aac534b75314595a
在 CKB-VM 中,也可以使用 syscalls [exec][exec] 和 [spawn (Draft)][spawn] 直接执行任意的 Script。区别是前者在执行完指定 Script 即结束,不会返回到调用者程序继续执行。Spawn 目前还未在测试网和主网中激活。它被包含在下一次的硬分叉中。
[exec]: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0034-vm-syscalls-2/0034-vm-syscalls-2.md#exec
[spawn]: https://github.com/zhangsoledad/rfcs/blob/zhangsoledad/vm-syscalls-3/rfcs/0147-vm-syscalls-3/0147-vm-syscalls-3.md#spawn
## ACL Centric Architecture
### Subject and Operation Recognition
正如 Background 中提到的,一个典型的ACL中的每个条目都指定了一个主体和一个操作。为了验证 ACL,必须先识别出当前交易中包含了哪些操作,这些操作的执行者是谁。这在当前环境下是非常具有挑战性的,因为 UTXO 模型中的交易只是列举了输入而输出,而并没有统一的方法能得知是什么操作导致了交易所涉及的改变。更具有挑战的是,一个交易中可能合并了多个操作的结果,而目前也没有一种通用的分组方案能把这些子操作给识别出来。
如果能形成规范,是能找到一些解决方案的,比如可以将交易中包含的操作放到 Witness,而主体就是 Witness 对应的 Script。如果使用 [Witness, OTX, and Typed Messages][witness-otx-typed-message] 中的设计,可以约定操作就是 `typed_message` 的 hash,而主体就是对应的 Type Script。比如下面的交易片段包含了一个操作,主体是 `hash(script-a)`,操作是 `hash(op-a)`。
```
witnesses:
0:
SighashWithAction:
message: op-a
inputs:
0:
type: script-a
```
但是问题是我们无法知道哪种规范是最优的,并且能够满足绝大数应用的需求。过早制定规范可能跟不上行业的快速发展,并且从 Witness 格式可以看到,在需要对规范进行升级时,是非常复杂和困难的。
重新回到主体和操作的识别,其实从验证的视角来看,整个过程是不关心具体是什么主体进行了什么操作的。比如说 Script A 授权给 Script B 进行 X 操作,和 Script C 进行 Y 操作。如果把这两项授权抽象成规则1,和规则2,实际上只需要验证规则1或者规则2返回真即可,而具体怎么识别出规则1和规则2的主体与操作,则可以在规则验证逻辑的内部去解决。而一段可在 CKB-VM 中执行的程序,结果能返回布尔值,正好对应了 CKB Script。所以授权就可以抽象成一列 Scripts,并且通过 NOT, AND, OR 等布尔操作符进行组合。比如一个 ACL 的列表可能是 `Rule1 OR (Rule2 AND NOT Rule3)`。 其中 Rule1, Rule2, Rule3 就是 3 个 CKB Scripts,它们负责检查在 tx 中是否包含了对应的主体和操作,比如 Rule1 可能表示 Script A 进行了 X 操作,而 Rule2 表示 Script B 进行了 Y 操作等等。
注意到使用 Script 来识别主体和操作,与上文中提到的规范并不冲突。我们完全可以根据规范开发对应的规则 Scripts。对于符合规范的 Cell,只需要通过这些已有的规则脚本来进行组合生成 ACL 即可,而对于不符合规范,或者新版本的规范,只需要新实现对应的规则 Scripts。规则 Scripts 的灵活性可以借助社区的力量,通过集体智慧进行不断迭代,最终涌现出一个最有效能的识别规范。
规则 Scripts 的还有一个好处就是抽象。每个 ACL 规则集本身也是一个返回布尔值的程序,所以它也能作为另外一个更大 ACL 规则集的成员。这种抽象能力能够将复杂度进行隐藏,大大降低系统集成的复杂度。
### Comoposition
当把主体和操作的识别抽象成规则 Scripts 之后,它们之间的组合就比较直接了,只需要支持常见的布尔操作即可。考虑到链上存储的成本和隐私性,可以使用(Rubin et al., 2014[^rubin2014MerkelizedAbstract]) 设计的 MAST 进行组合。这个结构和 [Combine Lock Script v0.1](https://www.notion.so/cryptape/Combine-Lock-Script-v0-1-e49f1f2de7504e8da1c9e6961309b317) 相同,但是抽象不同。
[^rubin2014MerkelizedAbstract]: Rubin, J., Naik, M., & Subramanian, N. (2014). Merkelized Abstract Syntax Trees.
### Model
ACL 的结构基本上与 [Enable Bitcoin Taproot on CKB (Part II)](https://blog.cryptape.com/enable-bitcoin-taproot-on-ckb-part-ii) 和 [Combine Lock Script v0.1](https://www.notion.so/cryptape/Combine-Lock-Script-v0-1-e49f1f2de7504e8da1c9e6961309b317) 相似,都是将可选的路径保存为 MAST。
ACL 的逻辑将实现为合约,以下被称为 ACL Script。ACL Script 有两种方式通过验证
1. Rule Path: 使用 ACL 规则进行验证
2. Fallback Path: 使用指定的 Fallback Script 进行验证
ACL Script 的 args 包含以下字段:
- Rules: 所有规则组成的 MAST Root Hash 或者是指向保存有 Root Hash 的 Cell 的 type script hash。使用 Type Script Hash 可以在 ACL Script 不变的情况下更新 MAST。
- Fallback Script (Optional): 使用 Fallback Path 进行验证,格式是序列化的 Script 结构
- Rule Path Witness Prefix (Optional): 指定当使用 Rule Path 时 Witness 必须添加的前缀。用于无法区分 Rule Path 和 Fallback Path 的 Witness 的情况。
例子1,不使用 Fallback,隐私性好,但是不便于钱包等应用。
```
Rules: hash(MAST)
Fallback Script: N/A
Rule Path Witness Prefix: N/A
```
例子2,使用 secp256k1 作为 Fallback。因为 Rule Path 的 Witness 和 secp256k1 签名长度不同,所以不需要再指定 witness prefix。
```
Rules: hash(MAST)
Fallback Script: secp256k1_lock pubkey_hash
Rule Path Witness Prefix: N/A
```
ACL Script 执行时,先读取 Witness,根据 Witness 判断是使用 Fallback Path 还是 Rule Path。
使用 Fallback Path 进行验证时,则反序列化出 Fallback Script 并使用 spawn syscall 执行。
使用 Rule Path 进行验证时,witness 格式如下:
```
Prefix: 如果 args 中有指定的话
Rule Proof: 用于证明 Rule 在 MAST 中
Rule Scripts: 用于验证 ACL Subject 和 Operation 的 Scripts
```
Rule Proof 的格式由 MAST 实现来确定。Rule Scripts 是一个以下结构的数组
```
Position: 在 MAST 中的位置
Script: ACL Script
Witness: 用于额外传给 ACL Script 的验证数据
```
ACL Script 必须先通过 Rule Proof 来验证 Rule Scripts 都存在于 MAST 中。然后执行 Rule Scripts 并将结果根据 MAST 进行组合来计算 MAST Root 对应的结果。如果结果为真则验证通过。注意,如果 Rule Scripts 中包含多个 Scripts,或者只有一个但是计算中需要使用 NOT 逻辑操作符,则必须要使用到 exec syscall。
## Applications
### Common Rule Scripts
ACL script `secp256k1_key` 允许使用 secp256k1 签名验证,和 default secp256k1 lock script 不同的地方在于获取 signature 的方式,ACL script 通过 args 读取 signature,而 Lock script 使用 witness。
```
name: acl_secp256k1_key
desc: 使用 secp256k1 签名通过验证,Witness 会拼接到 pubkey_hash 之后传递
args:
- pubkey_hash
```
ACL script `acl_always_success` 总是返回 true。使用 `always_success` 可以用于 Lock Script,实现用户将某些操作代理给 type script。即用户可以使用 Fallback Path 解锁,这时对应的 Type Script 应该有对应的 Rule Path 发行;而如果 Type Script 想跳过用户的确认,则可以使用 `acl_always_success` 的 Fallback Path。
```
name: acl_always_success
desc: 始终返回 true
args:
- any
```
### Session Key
```
MAST:
OR:
- acl_secp256k1_key session_pubkey_hash
Fallback Script: secp256k1_lock user_pubkey_hash
```
创建 Session Key 只要往 MAST 里插入 `acl_secp256k1_key` 以授权给 session key。
可以添加额外条件来限制 session key 的使用,比如 input cell 的 epoch number with fraction 小于某个值来限制使用时间。
```
MAST:
OR:
- AND:
- acl_secp256k1_key session_pubkey_hash
- acl_input_epoch_fraction less_than 1000+1/2
```
ACL script `acl_input_epoch_fraction` 通过 header deps 来读取 input cell 的 block header 中的 epoch 字段并于 args 进行判断
```
name: acl_input_epoch_fraction
desc: 读取 input cell 对应的 block header 中的 epoch 字段并于 args 比较
args:
- op: less_than, greater_than, ...
- value
```
这是一个简单的过期机制的实现,缺点是在过期之后还是能执行一次操作。一种解决方案可以参考 [CKB Expiry Mechanism](https://www.notion.so/cryptape/CKB-Expiry-Mechanism-00c1e08cfd7b48189b8364e53b631a63)
用户显示登出需要从 MAST 中删除对应的 session key ACL rule。
### ECS Architecture
假设要实现一个简单的放置游戏。
唯一的 Entity 是 Cube
- ID: 用类似 type id 的方式生成的唯一 ID。
- Kind: Cube 类型,比如土、树、水等。
- Position: 如果 Cube 被放置,表示其在世界中的坐标
- Player: 如果 Cube 没有被放置,表示其目前在哪位玩家的背包中
Cube 的 lock 和 type 都使用 ACL script。Lock 使用任意用户的 Lock Script 作为 Fallback,使用 `always_success` 作唯一的 Rule Path。
而 Type ACL script 则是通过 Rule Path 授权给 Systems 写权限。
**Mint System**: Mint System 负责生成新的 Cube。玩家需要使用自己的 CKB 来进行铸造。铸造逻辑存在于 Cube 的 Type ACL Script MAST 的某个 Rule Path 中。这个 Rule Path 会使用合约 `mint_system` 来验证铸造逻辑。因为 Mint 不需要解锁 Cube Cell 的 Lock Script,所以不需要相应的 Lock Script 的授权。
```
name: mint_system
desc: 遍历新的 Cubes (通过 ID 识别),验证 ID,Player 为空,Kind 和 Position 符合生成逻辑,lock/type script 符合要求
args: 提供证明,必要的 Rule Paths 存在于 lock/type script 的 MAST 中。 如果都使用 MAST Cell Type Script Hash 的话可以简化成验证都使用的是指定的 MAST Cell。
```
**Destroy System**: Destroy System 负责销毁 Cube。只有通过 Lock ACL Script 的 Fallback Path 才能销毁。
```
name: destroy_system
desc: 通过 ID 识别被销毁的 Cubes,这些 Cubes 使用 Fallback Path 解锁 Lock
args: N/A
```
**Transfer System**: 和 Destroy System 一样,也只能通过 Lock ACL Script 的 Fallback Path 授权
```
name: transfer_system
desc: 通过 ID 识别被更新的 Cubes,筛选出只有 Lock ACL 中的 Fallback Script 有变化的更新。 同时要求 Cubes 使用 Fallback Path 解锁。
args: N/A
```
**Mine and Place System**: 这两个 Systems 一个把放置的 Cube 放入背包,一个把背包中的 Cube 放置到世界中。Mine 和 Place 都无需 Cell Owner 确认,Place 需要背包的所有者确认。
```
name: mine_system
desc: 通过 ID 识别被更新的 Cubes,通过 Position 和 Player 字段出筛选出 mine 操作,并检查 mine 操作符合逻辑。 Lock Script 可以使用 always_success,但是 mine_system 里面不需要检查。
args: N/A
```
```
name: place_system
desc: 将背包中方块放置到世界中。 通过 ID 识别被更新的 Cubes,通过 Position 和 Player 字段出筛选出 place 操作。 Lock Script 可以使用 always_success,但是 place_system 里面不需要检查。 需要调用 Player 里指定的 script 验证背包所有人授权了该操作。
args: 传给 Cube Player script 的 witness
```
Place 是目前唯一需要用户进行确认的高频操作,所以 Player Script 可以使用上面提到的 Session Key 模式进行自动确认。
综上,Type ACL Script 被拆分成若干的 Systems:
```
MAST:
OR:
- mint_system
- destroy_system
- transfer_system
- mine_system
- place_system
```
这些 systems 可以分组到几个 Scripts 方便代码共享,比如 Mine 和 Place。使用时通过 args 来区分。
Cube 的 Lock ACL Script 是
```
Fallback Script: Owner
MAST:
OR:
- acl_always_success
```
另外注意到,选择哪一条 Rule Path 其实也相当于声明了对 Cell 执行了什么操作。而且是有可能把多个操作放到一个交易里去执行,不过需要更仔细的设计 MAST 的执行,比如定义 MAST 使用了OR,但是执行的时候可能想要的效果是所有列出来的 Rule Path 都成功,而不是只需要一个成功。
### Payment Channel
再来看看 [Perun](https://www.notion.so/cryptape/Perun-Contracts-Review-Report-d5f6f51ee4ea4d3f833b5d1deb73e96d) 应该怎么实现。在 Perun 分析里可以知道它是一个典型的状态机,只需要把 Transitions 拆分成 MAST Rules 即可,这里就不展开了。
## Result
可以看到,从 ACL 的角度出发去思考 Scripts 间的合作问题,总体上还是没有走出 Taproot/Combine Script 这些模式。不过从报告上可以看出,在 MAST 的基础上,是可以总结出一些开发模式的,比如把 MAST 中的 Spend Path 作为 Intent/Action,
通过 MAST 这一中间层可以将复杂应用进行模块化再进行组合。这一中间层同时提供了一定的灵活性:
1. 模块可以单独进行升级
2. 可以支持第三方的模块扩展
3. 支持模块重用
## Future Work
- 打磨成一篇对外分享的文章 (Est. 1 week)
- 完善放置游戏的需求并开发成 Demo (Est. 2 weeks)