CKB 脚本编程简介[6]: Type ID

Xuejie Xiao

Xuejie Xiao

Nervos Core Team

你可能注意到 CKB 的脚本结构中还有一个 hash_type 的字段。之前为了简化,我们一直忽略它,直到现在。本文将介绍一下 hash_type 字段,以及它所能带来的独特能力。

问题

在区块链的世界里,有两个问题是每个人都必要面对的:

可升级性

在一个智能合约部署在区块链上之后,我还能升级它吗?假设一个智能合约得到了广泛采用,然后突然有人在这个智能合约中发现了一个 bug(遗憾的是,这样的情况在区块链行业总是会发生),我们能否在不影响所有用户的情况下,将智能合约升级到一个修复完成的版本呢?

另一种情况是:随着技术的进步,可能会有新的算法出现,用它可以构建运行速度更快的智能合约。此时,我们可以升级现有的智能合约,来让更多用户受惠吗?

确定性

这里有两个部分:

  • 确定性 A:如果我选择一个智能合约来保护我的 tokens,那么我的 tokens 在未来也会安全吗?(可以由我解锁,且只能由我解锁)
  • 确定性 B:如果我现在签名了一笔交易,然后稍后再发送它,我的交易还会被区块链接受吗?

注意,一个安全的区块链要比这里提到的内容有更多的确定性需求。在这里我只描述和讨论的问题相关的属性。

冲突

如果我们仔细想想,我们就会发现,可升级性和确定性之间总是存在冲突:

  • 如果一个智能合约可以升级,那它可能具有不同的行为,从而使攻击者能够解锁这个 cell,或者禁止 token 所有者去解锁这个 cell。
  • 如果一个智能合约可以升级,那么一笔已经签名的交易可能会(在升级前后)执行不同的行为,从而被区块链拒绝。

回看过去,在可升级性和确定性之间,你只能选站一边。并且现在大多数现有的区块链项目都选择了确定性这一边。“代码即法律”的思想在区块链领域非常流行。但是我们都知道软件的设计是一个权衡的过程。在特定情况下,牺牲一些确定性来换取可升级性带来的便利是有意义的。例如,一个大型组织可能会有专门的安全团队来监视他们所使用的智能合约中潜在的漏洞。通过授权安全团队升级智能合约的能力,他们就可以自己掌控漏洞的修复,而不用等待别人。同时,确认性中的属性 A 对他们来说并不是问题,因为他们会对自己的 Cells 和智能合约进行负责。

所以现在的问题在于:我们能在 CKB 中实现这种新的可能性吗?当然可以,而且整个机制已经部署在了现在的 CKB 主网上。但是要理解在 CKB 上的这个机制,我们得先看看其他支持这个机制的想法。

注意:我想你肯定会问:这是否意味着你需要在 CKB 上牺牲确定性?我向你保证,在 CKB 上,这个功能完全是可选的,你可以像在其他区块链中一样,在 CKB 上完美地践行“代码即法律”的原则。我们只是希望这个独特的功能,能为那些真正需要它的人提供新的可能性。

编写一个独一无二的 Type Script

让我们先问一个问题:我们如何创建一个 type script,以确保在整个 CKB 中,只有一个 live cell 可以拥有这个独一无二的 type script?我们所说的独一无二的 type script 是指整个 type script 结构,包括 code hash,hash type 和 args。

请注意,这个问题现在看起来可能和脚本的可升级能力没什么关系,但是请相信我,耐心一点,我们将看到它是如何帮助在 CKB 上实现最终的解决方案。

如果你看过我之前关于 UDT 的文章,你可能已经意识到了一个解决方案。但是如果你还没有看过,我建议你先去阅读一下,如何思考一下,如果现在换做是你自己,你将如何实现这样的脚本。这将是一次很好的学习经历。

如果你准备好了,下面就是脚本的基础的工作流程:

  1. 计算有多少个 output cells 使用当前的 type script,如果有多个 cell 使用当前的 type script,则返回一个失败的返回码;
  2. 计算有多少个 input cells 使用当前的 type script,如果只有一个 input cell 使用当前的 type script,则返回一个成功的返回码;
  3. 使用 CKB syscall 读取当前交易中的第一个 OutPoint;
  4. 如果读取的 OutPoint 数据与当前 type cript 的 args 部分匹配,则返回成功的返回码;
  5. 否则返回失败的返回码。

将上述逻辑通过一个简单的 C 代码实现,脚本如下:

正如在 UDT 文章中所解释的,从几个不同的方向上去阻止攻击者:

  1. 如果攻击者试图使用完全一样的 type script 创建一个 cell,这将会出现两种情况:a. 一笔有效的交易在 type script args 的第一个输入中会有不同的 OutPoint 数据;b. 如果用户试图复制 type script args 的第一笔交易输入,CKB 将报出双花的错误。
  2. 如果攻击者试图使用一个不同的 type script args,那么根据定义,它将是一个不同的 type script。

通过这种方式,我们可以确保在 CKB 上所有的 live cells 中,只有一个 cell 会拥有这个独一无二的 type script。考虑到每个脚本都有一个相关联的哈希,我们将在 CKB 中只有一个 cell 拥有这个独一无二的 hash,或者说独一无二的 ID。

解析 CKB 交易中的脚本

现在让我们看看在 hash type 改变之前,CKB 是如何解析脚本并运行的:

  • CKB 从脚本结构中提取 code hash 值并运行。
  • 它将循环遍历所有的 dep cells,计算每个 dep cell 的 data hash。如果有任何一个 dep cell 的 data hash 与指定的 code hash 相匹配,CKB 将使用找到的 dep cell 中的数据作为脚本然后去运行。
  • 如果没有 dep cell 拥有和指定的 code hash 相匹配的 data hash,CKB 将提示验证错误。

可升级性的问题,实际上在于我们测试 dep cells 的方式。现在我们正在对(dep cells 的)data hash 进行测试,当脚本升级时,将生成一个不同的 hash,匹配将失败。这就带来了一个问题:我们能不能采用一种不同的方案来测试 dep cells ?当脚本改变时,有什么东西是可以保持不变的?考虑到在 cell 中运行的实际脚本,我们可以用另一种方式来重新表述这个问题:

当一个 cell 的数据更新时,有什么东西是可以保持不变的?

我们可以使用脚本结构!因为 lock script 通常用于签名验证,我们可以使用一个 type script 来解决这个问题。当一个 cell 的数据发生改变时,type script 可以完全保持不变。因此,我们在 CKB 的脚本结构中加入 hash type 字段,并将脚本的解析流程修改为:

  • 对于每个 dep cell,我们在脚本结构中基于 hash type 的值提取 test hash:
    • 如果 hash typedata,则 dep cell 的 data hash 将作为 test hash
    • 如果 hash typetype,则 dep cell 的 type script hash 将作为 test hash
  • CKB 从脚本结构中提取 code hashhash type 的值并运行
  • 如果 CKB 找到一个 dep cell 的 test hash 和指定的 code hash 相匹配,那么 CKB 使用所找到的 dep cell 中的数据作为脚本并运行
  • 如果没有 dep cell 具有和指定的 code hash 相匹配的 test hash,那么 CKB 将提示验证错误。

注意这里使用的 hash type,是属于要运行的脚本的值,而不是 dep cell 中脚本的值。你完全可以在一笔交易中拥有两个 inputs,一个使用 data 作为 hash type,另一个使用 type 作为 hash type。它们中的任何一个都可以使用它自己正确的方式来定位到正确的 cell。

通过这种方式,我们就完全克服了上面提到的确定性中的属性 A,但是对于属性 B,会有一些微妙的影响,我们将在下面的内容里详细讨论。

把所有都放在一起

还有一个问题没有解决,还是可能会被攻击:

  • 一个 lock script L1 存储在 cell C1 中,type script 为 T1
  • Alice 通过 lock script L1 使用 hash type 为 `type 来保护自己的 cells,根据定义,她使用 type script T1 的 hash 来填充她的脚本架构中的 code hash` 字段。
  • Bob 创建另一个 cell C2,它的 lock script L2 为 always success,并且使用相同的 type script T1。
  • 现在 Bob 可以使用 C2 作为 dep cell 来解锁 Alice 的 cell。CKB 无法区分是不是 Alice 想要使用 C1,然而 Bob 提供了 C2。C1 和 C2 都使用 T1 作为 type script。

这给我们上了非常重要的一课:如果你构建了一个 lock/type script,并且希望人们利用其可升级的属性,你必须确保使用的 type script 是独一无二且不可伪造的。

幸运的是,我们刚刚解决了这个问题!在上面的例子中,我们开发了一个 type script,它可以在 CKB 中提供一个独一无二的 type script。结合在一起,这个独一无二的 type script 和 hash type,为 CKB 提供了一种可以升级已部署的智能合约的方法。

鸡生蛋,蛋生鸡的问题

你可能已经注意道理,CKB 已经实现了这样一个脚本,名为 type ID 的脚本。但是与其他系统脚本不同的是,这个脚本是在 CKB VM 外完全用 Rust 实现的。我相信你可能会有这样的抱怨:你不是说过 CKB VM 是非常灵活的吗?

我确实在很多不同的场合说过那样的话,实际上,我在上面已经向你展示了一种用 C 语言实现 Type ID 逻辑的方法,它可以在 CKB VM 中编译且运行。但是这里的问题是,我们正处于一个鸡生蛋、蛋生鸡的问题:如果我们实现了 type ID 脚本,它是否应该是可升级的?如果是,我们应该把上面作为它的 type script?如果它不能升级,我们又如何确保它没有 bugs 和其他漏洞?

我们认真地考虑了这个问题,通过纯 Rust 实现 type ID 脚本,这看起来确实是最简单和最安全的解决方案,这样我们可以以一种更成熟和更有经验的方式处理任何潜在的情况。

但是,与其他一些为了内置智能合约而不断做出妥协不同,我们在 type ID 脚本中设置了一个非常大的 cycle 花费,来阻止内置脚本的使用。在主网中,它被设置为 100 万个 cycles。这养的花费,比在 CKB 上实现相同的逻辑要大的多的多,我们希望有一天会有完全审查且安全的 Type ID 脚本,每个人都使用这个脚本而不是内置的 Type ID 脚本。

信任问题

我们要提醒你,这个新特性并非没有确点。在使用它之前,你应该真正了解它是如何工作的以及所涉及到的权衡。本小节试图讨论所有这些注意事项,但是我要提醒你,它可能还是会遗漏一些内容,你需要自行判断。

确定性属性 A

Type ID 的解决方案确实提供了一种解决属性 A 的方法:当发现 bug 时,可以修复智能合约,而不影响使用相同智能合约的现有 cells。但它确实需要注意一些事宜:

所有权

有了 type ID 的解决方案,人们可能可以通过黑进包含你使用智能合约的 cell 来窃取你的代币。由于一个典型的 cells 仅由一个或者多个签名进行保护,一些人为的错误可能会导致重大问题。从根本上来说,这是一个折衷的情况:

  1. 对于真正多疑的人,他们可能希望坚持使用通过自身的 hash 来引用脚本的老办法,而不是通过包含 cell 的 type script hash。“代码即法律”的原则将在这里得到充分执行。你可以确切地知道什么可以用来解锁你的 cell,这在将来的任何时候都不会改变。
  2. 对于愿意做出一些牺牲的人来说,他们可以通过升级现有的智能合约来获得不同的收益。但是请确保你可以完全理解包含你使用的脚本的 cell 的 lock script。

例如,如果你查看 CKB 主网上部署的系统脚本,他们都使用 type ID 设置,但是它们的 lock script 的 code hash 都被设置为 0,这意味着没有人可以通过签名轻易地改变 lock script。我们需要确保当有必要改变默认的 lock script 时,它需要通过一次分叉基于整个社区的共识。在我看来,毕竟是 Nervos 社区真正地拥有 CKB。

你可以想一些其他的想法来保护 cell 中包含的脚本。例如,除了 type ID 的逻辑之外,type script 还可以包含其他逻辑,以验证其 cell 中实际包含的脚本。例如,可以使用形式化分析的方法,或者可以提供某些测试用例,在这些测试用例中,type script 可以运行实际的脚本。只有当分析或测试用例通过时,才能够更改 cell 中包含的脚本。但是我必须说,这都是一些初步的想法,想要使其成为现实,还需要做大量的研究和开发工作。

可用性

Type ID 的另一个问题是可用性。当你有一个使用 hash type 设置为 data 的脚本的 cell 时,你不必担心有人可能会破坏使用过脚本,你总是可以在链上重新部署脚本,然后解锁你的 cell。

但是对于 type ID 的解决方案,如果发生了糟糕的事情,并且具有相同的 type script hash 的 cell 被破坏,你的 cell 将被永久锁定,因为你可能永远无法构建具有相同 type script hash 的 cell。你可以使用一些方法,例如限制使用 type ID 脚本销毁 cell 的能力,但是这些方法都只能缓解问题,而不能完全解决问题。

确定性属性 B

我们需要指出的是,type ID 并不能为属性 B 提供解决方案。当你有一个已签名的交易时,此交易的行为不会随着智能合约的升级而改变。

尽管 type ID 的解决方案可能会影响已经签名的交易:如果交易使用来自 cell 的脚本,并且 cell 得到更新,则交易将被判定为无效,因为原始 OutPoint 中引用的 cell 已经被花费了。有人可能会因为旧的引用方案而发生争论,这个问题也可能发生。

结论

这就是我目前所知道的,关于 type ID 的所有信息 :) 它当然有它的缺点,但是我们相信它对于某些用户来说将会是非常有用的。而对于其他用户,这是完全可选的,你完全忽略了这个特性。我希望这一个在其他区块链中很少出现的新功能,能够作为一个起点,以推动更多的可能性。

原文链接