附录: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 规则。
- 注意:这里只要求存在,不要求唯一,也不需要考虑 CoBuild 模式下,不同的
- 否则,使用 legacy flow 进行校验。
Lock Script
CKB lock script 的主要任务是进行所有权(所有者签名)验证——校验 input cell 以正确的条件被解锁,同时也确保交易中的数据不被篡改。一个同时支持 SighashAll 以及 Otx 模式的 lock script,应按照如下规则对交易进行校验:
- 维护一组变量 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
- 循环扫描当前 tx 的所有 witnesses (循环变量 i),检查当前 tx 中,是否有一个 witness,使用
WitnessLayout::OtxStart类型 - 如果当前 tx 中不存在
WitnessLayout::OtxStart类型的 witness,跳到 step 8 继续执行验证 - 确保当前 tx 中只有一个 witness 使用
WitnessLayout::OtxStart结构,否则报错退出。并把当前循环变量 i 设置为该 witness 的index值。 - 把 is 和 ie 置为
OtxStart中的start_input_cell值,把 os 和 oe 置为OtxStart中的start_output_cell值,把 cs 和 ce 置为OtxStart中的start_cell_deps值,把 hs 和 he 置为OtxStart中的start_header_deps值 - 从
index为 i + 1 的 witness 开始,循环遍历每一个类型为WitnessLayout::Otx的 witness (如果遇到类型不为WitnessLayout::Otx的 witness 就直接跳出循环),并对每一个WitnessLayout::Otxwitness(对应 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
- i. index 在
- 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 与Action中script_hash相同,要么存在一个 input cell,它的 lock script hash 与Action中script_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
- a. 假设
- 假设从
index为i + 1的 witness 开始,第一个不为WitnessLayout::Otx类型的 witness,index为j。检查当前 tx 中,index 在[0, i)以及[j, +infinity)范围内的 witness 都不是WitnessLayout::Otx类型,否则报错退出。 - 检查
index在[0, is)以及[ie, +infinity)范围内的 input cells,如果其中有任意一个使用了当前 lock script,则执行一次SighashAll验签操作:- a. 检查当前 tx 的所有 witnesses 中是否有一个是
WitnessLayout::SighashAllvariant. - 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 与Action中script_hash相同,要么存在一个 input cell,它的 lock script hash 与Action中script_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进行验签。如果验签失败,则报错退出。
- a. 检查当前 tx 的所有 witnesses 中是否有一个是
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 进行校验:
- 执行 type script 自定义状态迁移逻辑的校验。
- 维护一组变量 is, ie, os, oe, cs, ce, hs, he, 并全部置为 0。
- 循环扫描当前 tx 的所有 witnesses (循环变量 i),检查当前 tx 中,是否有一个 witness,使用
WitnessLayout::OtxStart类型。 - 如果当前 tx 中不存在
WitnessLayout::OtxStart类型的 witness,跳到 step 9 继续执行验证。 - 确保当前 tx 中只有一个 witness 使用
WitnessLayout::OtxStart结构,否则报错退出。把变量 i 设置为该 witness 的index值。 - 把 is 和 ie 置为
OtxStart中的start_input_cell值,把 os 和 oe 置为OtxStart中的start_output_cell值,把 cs 和 ce 置为OtxStart中的start_cell_deps值,把 hs 和 he 置为OtxStart中的start_header_deps值 - 从
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
- i. index 在
- 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
- a. 假设
- 假设从
index为i + 1的 witness 开始,第一个不为WitnessLayout::Otx类型的 witness,index 为j。检查当前 tx 中,index在[0, i)以及[j, +infinity)范围内的 witness 都不使用WitnessLayout::Otx类型,否则报错退出 - 检查
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进行校验(具体校验过程参考下一小节)
- a. 扫描当前 tx 的所有 witnesses,判断当前 tx 中,是否有一个 witness 是
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 在验签过程中覆盖以确保无法篡改。 - 在生成签名的过程中,钱包应该使用正确的
ScriptInfo对Action.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 返还给了支付方。