附录: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::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. 检查
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::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 与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 返还给了支付方。