CKB Transaction CoBuild Protocol Overview

附录:Lock/Type Script Validation Rules

本节描述符合 CoBuild 流程要求的 lock/type script 需要包含的校验规则。

从方便和实用的角度,我们推荐所有的 lock/type scripts 在支持 legacy WitnessArgs 的基础上,先支持 SighashAll Variant,再支持 Otx Variant。

Legacy Flow Handling

考虑到向前兼容,支持 CoBuild 流程的 lock / type script 很可能也需要支持 legacy flow,即以 WitnessArgs 结构保存 witness 数据的现有流程。

一个 lock / type script 可以同时支持 CoBuild flow 和 legacy flow。但是对于某个特定的交易,一个 lock / type script 只能选取其中一种 flow 来进行校验:要么使用 CoBuild flow,要么使用 legacy flow。

通常情况下,一个 Transaction 中的多个 script 应该都使用同一种 flow 来校验,但是有一种情况例外:在一个 Transaction 中混用支持两种 flow 的 script A,以及只支持 legacy flow 的 script B 时,可能会发生 script A 按照 CoBuild flow 来校验,而 script B 因为只支持 legacy flow 而按照 legacy flow 来校验的情况。

Lock / type script 应按照如下的规则判断在当前交易中,应使用哪种 flow:

  • 如果当前交易中存在一个使用 WitnessLayout 结构的 witness 时,则使用 CoBuild flow 进行校验;
    • 注意:这里只要求存在,不要求唯一,也不需要考虑 CoBuild 模式下,不同的 WitnessLayout 结构彼此之间是否可以共存,是否符合 CoBuild 规则。
  • 否则,使用 legacy flow 进行校验。

Lock Script

CKB lock script 的主要任务是进行所有权(所有者签名)验证——校验 input cell 以正确的条件被解锁,同时也确保交易中的数据不被篡改。一个同时支持 SighashAll 以及 Otx 模式的 lock script,应按照如下规则对交易进行校验:

  1. 维护一组变量 is, ie, os, oe, cs, ce, hs, he, 并全部置为 0.
    • is: input start
    • ie: input end
    • os: output start
    • oe: output end
    • cs: cell dep start
    • ce: cell dep end
    • hs: header start
    • he: header end
  2. 循环扫描当前 tx 的所有 witnesses (循环变量 i),检查当前 tx 中,是否有一个 witness,使用 WitnessLayout::OtxStart 类型
  3. 如果当前 tx 中不存在 WitnessLayout::OtxStart 类型的 witness,跳到 step 8 继续执行验证
  4. 确保当前 tx 中只有一个 witness 使用 WitnessLayout::OtxStart 结构,否则报错退出。并把当前循环变量 i 设置为该 witness 的 index 值。
  5. 把 is 和 ie 置为 OtxStart 中的 start_input_cell 值,把 os 和 oe 置为 OtxStart 中的 start_output_cell 值,把 cs 和 ce 置为 OtxStart 中的 start_cell_deps 值,把 hs 和 he 置为 OtxStart 中的 start_header_deps
  6. index 为 i + 1 的 witness 开始,循环遍历每一个类型为 WitnessLayout::Otx 的 witness (如果遇到类型不为 WitnessLayout::Otx 的 witness 就直接跳出循环),并对每一个 WitnessLayout::Otx witness(对应 tx 中包含的每一个 Otx),进行如下校验:
    • a. 假设 otx 变量保存 WitnessLayout::Otx 类型的 witness,那么以下数据构成当前 Otx (open transaction):
      • i. index 在 [ie, ie + otx.input_cells) 区间内的 input cells
      • ii. index 在 [oe, oe + otx.output_cells) 区间内的 output cells
      • iii. index 在 [ce, ce + otx.cell_deps) 区间内的 cell deps
      • iv. index 在 [he, he + otx.header_deps) 区间内的 header deps
      • v. 当前在处理的, WitnessLayout::Otx 类型的 witness
    • b. 检查 WitnessLayout::Otx 类型的 otx 变量中的内容,如果 otx.input_cells, otx.output_cells, otx.cell_deps, otx.header_deps 全部为 0,script 应该报错退出。
    • c. 解析 WitnessLayout::Otx 类型的 otx 变量中的 message 结构,针对 message 中包含的所有 Action ,检查当前 CKB transaction 中,要么存在一个 input / output cell,它的 type script hash 与 Actionscript_hash 相同,要么存在一个 input cell,它的 lock script hash 与 Actionscript_hash 相同,否则报错退出。
      • 注意这里的检查范围是完整的 CKB transaction,并不只是当前的 Otx。
    • d. 检查 index[ie, ie + otx.input_cells) 中的 input cells,如果所有的 input cells 都不使用当前的 lock script,则跳到循环的下一个 iteration 继续执行 (i.e. continue).
    • e. 根据 Appendix: CoBuild Hashes 中定义的计算规则,根据 Otx 计算对应的 signing message hash.
    • f. 以当前 lock script hash 为 key,从 seals 中取出对应当前 lock script 的 SealPair , 如果找不到,则报错退出。
    • g. 从 SealPair 中根据 lock script 的自定义逻辑取出签名,针对前面生成的 signing message hash 进行验签。如果验签失败,则报错退出。
    • h. ie += otx.input_cells; oe += otx.output_cells; ce += otx.cell_deps; he += otx.header_deps
  7. 假设从 indexi + 1 的 witness 开始,第一个不为 WitnessLayout::Otx 类型的 witness,indexj。检查当前 tx 中,index 在 [0, i) 以及 [j, +infinity) 范围内的 witness 都不是 WitnessLayout::Otx 类型,否则报错退出。
  8. 检查 index[0, is) 以及 [ie, +infinity) 范围内的 input cells,如果其中有任意一个使用了当前 lock script,则执行一次 SighashAll 验签操作:
    • a. 检查当前 tx 的所有 witnesses 中是否有一个是 WitnessLayout::SighashAll variant.
    • b. 如果存在一个 WitnessLayout::SighashAll 类型的 witness ,确保当前 tx 中只有一个 witness 是 WitnessLayout::SighashAll 类型,并取出 witness.message 保存到 message 变量待后续使用;如果当前 tx 中没有 WitnessLayout::SighashAll 类型的 witness,则把 message 变量设为空。
    • c. 假设 message 不为空,对 message 中包含的每一个 Action ,检查当前 CKB transaction 中,要么存在一个 input / output cell,它的 type script hash 与 Actionscript_hash 相同,要么存在一个 input cell,它的 lock script hash 与 Actionscript_hash 相同,否则报错退出。
    • d. 读取当前 lock script 对应 script group 中的第一个 witness,如果该 witness 不为 WitnessLayout::SighashAll 类型,也不为 WitnessLayout::SighashAllOnly 类型,则报错退出。
    • e. 确保当前 lock script group 中除了第一个 witness 的位置,要么不存在 witness,要么为空。
    • f. 从当前 lock script group 的第一个 witness 中取出 seal,并按照 lock script 的自定义逻辑取出签名。
    • g. 依照 Appendix: CoBuild Hashes 的计算规则,计算当前 SighashAll / SighashAllOnly 结构对应的 signing message hash
    • h. 对从 seal 中取出的签名以及 signing message hash 进行验签。如果验签失败,则报错退出。

Type Script

CKB type script 主要负责链上状态迁移的逻辑校验。例如在 UDT 中,type script 验证不能增发。在 CoBuild 流程中,type script 除了自定义业务逻辑校验之外,也要负责校验 full tx / otx 中,message 与交易内容的一致性。对于数据逻辑本身的验证,type script 可以忽略 otx 的存在,但是在 message 校验中,type script 不仅需要对每一个 otx 分别校验,还要对可能存在的 SighashAll witness 中包含的 message 进行校验。

一个同时支持 SighashAll 以及 Otx variant 的 type script,应该按照如下的规则对 CKB Transaction 进行校验:

  1. 执行 type script 自定义状态迁移逻辑的校验。
  2. 维护一组变量 is, ie, os, oe, cs, ce, hs, he, 并全部置为 0。
  3. 循环扫描当前 tx 的所有 witnesses (循环变量 i),检查当前 tx 中,是否有一个 witness,使用 WitnessLayout::OtxStart 类型。
  4. 如果当前 tx 中不存在 WitnessLayout::OtxStart 类型的 witness,跳到 step 9 继续执行验证。
  5. 确保当前 tx 中只有一个 witness 使用 WitnessLayout::OtxStart 结构,否则报错退出。把变量 i 设置为该 witness 的 index 值。
  6. 把 is 和 ie 置为 OtxStart 中的 start_input_cell 值,把 os 和 oe 置为 OtxStart 中的 start_output_cell 值,把 cs 和 ce 置为 OtxStart 中的 start_cell_deps 值,把 hs 和 he 置为 OtxStart 中的 start_header_deps
  7. index 为 i + 1 的 witness 开始,循环遍历每一个类型为 WitnessLayout::Otx 的 witness(当遇到类型不为 WitnessLayout::Otx 的 witness 就直接跳出循环),并对每一个 WitnessLayout::Otx 类型的 witness(对应 tx 中包含的每一个 Otx),进行如下校验:
    • a. 假设 otx 变量保存了 WitnessLayout::Otx 类型的 witness,那么以下数据构成当前的 Otx (Open Transaction):
      • i. index 在 [ie, ie + otx.input_cells) 区间内的 input cells
      • ii. index 在 [oe, oe + otx.output_cells) 区间内的 output cells
      • iii. index 在 [ce, ce + otx.cell_deps) 区间内的 cell deps
      • iv. index 在 [he, he + otx.header_deps) 区间内的 header deps
      • v. 当前在处理的, WitnessLayout::Otx 类型的 witness
    • b. 检查 index[ie, ie + otx.input_cells) 中的 input cells,以及 index[oe, oe + otx.output_cells) 中的 output cells,如果这些所有的 input / output cells 都不使用当前的 type script,则跳到循环的下一个 iteration 继续执行(i.e. continue)。
    • c. 以当前的 Otx 范围,对 WitnessLayout::Otx 中包含的 message 进行校验(具体校验过程参考下一小节)
    • d. ie += otx.input_cells; oe += otx.output_cells; ce += otx.cell_deps; he += otx.header_deps
  8. 假设从 indexi + 1 的 witness 开始,第一个不为 WitnessLayout::Otx 类型的 witness,index 为 j。检查当前 tx 中,index[0, i) 以及 [j, +infinity) 范围内的 witness 都不使用 WitnessLayout::Otx 类型,否则报错退出
  9. 检查 index[0, is) 以及 [ie, +infinity) 范围内的 input 和 output cells,如果这些 input 和 output cells 中,存在使用当前 type script 的任意一个 cell,进行如下校验:
    • a. 扫描当前 tx 的所有 witnesses,判断当前 tx 中,是否有一个 witness 是 WitnessLayout::SighashAll 类型
    • b. 如果当前 tx 中不存在 WitnessLayout::SighashAll 类型的 witness,则认为当前合约校验成功,以 0 作为 return code 退出执行
    • c. 检查当前 tx 中有且只有一个 witness 是 WitnessLayout::SighashAll 类型,否则报错退出。
    • d. 以 index 在 [0, is) 以及 [ie, +infinity) 内的 input cells,和 index 在 [0, os) 以及 [oe, +infinity) 内 output cells 作为校验范围,对 WitnessLayout::SighashAll 中包含的 message 进行校验(具体校验过程参考下一小节)

Message Validation

本节描述 type script 对 message 的校验过程。

无论是 Otx 还是 SighashAll 模式的交易,都要针对一个范围内的 input / output cells,对 message 中属于当前 type script 的那部分信息(包含在一个 Action 中),进行校验。

注意这里提到 “一个范围” 的 cells:因为 otx 的存在,所以这里的范围不一定是当前的完整 tx,可能是比如 [3, 6) 的这个区间,或者是 [0, 4) || [8, 11) 的这样两个区间的并集。但是最多只会有两个区间的并集,不会有更多区间。

type script 需要遍历 message 中包含的 actions,找到一个 Action.script_hash 与自身的 script hash 相同的 Action,这个 Action 即为对应当前 type script 的 Action。

  • 如果发现与自身 script hash 对应的多个 Actions, 推荐做法是报错。这样处理的目的是防止第三方在用户构造 Action 之后往交易里面插入调用同样的 type script 的 Action 覆盖用户 Action。
    • 第三方 Action 有可能在 wallet UX 上显示在用户 Action 后面甚至由于展示空间不够被挡住,导致用户以为要签名的只包含自己的 Action。
  • 参考代码

需要注意的是:链上的 type script 无法获取当前 Action 的完整 ScriptInfo,也无法对 Action.script_info_hash 的正确性进行校验。

因此 Type script 可以做如下两点假设:

  • 整个 Action 的内容,包含 Action.script_info_hash ,都被 lock script 在验签过程中覆盖以确保无法篡改。
  • 在生成签名的过程中,钱包应该使用正确的 ScriptInfoAction.script_info_hash 进行校验,并通过 ScriptInfo.schema 正确呈现 Action.data 中的内容给持有该 cell 的用户进行了确认。

虽然 type script 没有完整的 ScriptInfo,但是 type script 应该在源码中以某种方式包含了 ScriptInfo.schema 以及 ScriptInfo.message_type 的内容。Type script 可以借助这些内容解析 Action.data , 还原出对应的输入数据,并以此对特定范围内的 cells 进行校验。例如,如果 Action.data 是一个 Spore NFT Mint 操作,type script 就要校验当前范围内的 cells 的确是一个 mint 操作;如果 Action.data 是一个 UDT transfer 操作,type script 也要进行类似的校验,确保正确数量的 UDT 被转给了正确的收款方,多余的 UDT 返还给了支付方。