Open Tx 协议头脑风暴[2]: 在 CKB 上设计一个实用的协议
Cipher Wang
Nervos Core Team动机
为 Open TX
设计一个协议,我们需要遵循一些基本原则,比如签名规则,组合规则和安全问题。在本文中,我想谈谈这些问题。
签名规则
通常,每个有意义的 lock script
都需要一个非对称加密签名来解锁。对于默认的 SIGHASH_ALL
方案,它首先会计算完整的交易的哈希,任何对哈希结果进行签名。对于 OTX,为了确认所有权,它的签名必须覆盖每一个 OTX 的输入和它所涉及的字段。
如上图所示,一个完整的交易由两个 open TXs 组成,OTX#0
和 OTX#1
。如果这是一笔传统的完整的交易,并假设所有的输入都使用同样的 lock script,那么它的哈希应该覆盖交易内的所有数据。但是对于 open TX 方案来说,这里有两个 OTX 有它们各自的签名来包含内部的条件,从而形成一笔完整的交易。
让我们以一个最简单的场景为例。Alice 有 100 个 USDT,放在了 input#0
中,然后她要发送给 Bob 20 USDT,找零的 80 USDT 需要放在 output#1
中。这个 OTX#0
是无效的,因为:
- 它产生了一个额外的输出 cell,而这个 cell 需要额外的 CKBytes 来进行创建(比如说需要用 100 CKBytes)
- 它不提供任何转账手续费。其他人需要将
OTX#1
与OTX#0
组合在一起,从而形成一笔有效的交易。比如,它用 1000 CKBytes 作为输入,890 CKBytes 作为输出。所以,OTX#1
其实为output#1
提供了 100 CKBytes,同时也提供了 10 CKBytes 作为转账手续费。
在这个过程中,OTX#0
的签名应该包括 input#0
,output#0
和 output#1
,而 OTX#1
则应该包括 input#1
和 output#2
。这里的签名有两个目的:
- 判断输入的所有权
- 确保 TX/OTX 的完整性
为了实现这些,签名应该包括即将被花费的输入和签名者关心的交易中的其他部分。这些内容的范围定义有四个不同的级别。
范围级别 0,TX(交易)
这意味签名包含了一笔交易的所有数据,换句话说,这个级别的签名与比特币中 SIGHASH_ALL
是一样的。它从交易中加载所有的数据,然后进行哈希并签名。没有人可以修改这笔交易,因此它不能支持 open tx 方案。
范围级别 1,Cells
在这个级别上,OTX 的签名包括了特定的 cells,就像上面演示的那样,第一个 OTX 对一些输入和输出进行签名,第二个 OTX 对剩下的进行签名,然后它们可以相互组合,最终形成一笔有效的交易。
这中模式需要一个很好的 OTX 定义方法来识别它所设计的 cells。例如,每个签名都需要附上一个 cell 索引位图作为参数(在 witnesses 字段中)来描述它所包含的 cells。
范围级别 2,Fields(字段)
在这个级别上,OTX 定义在一笔交易的 cells 中哪些字段是它所关心的。例如,它约束了 output#0
的 type script
,data
和 lock script
的 code hash
字段,而不去管 lock script
的 args
。
范围级别 3,Logics(逻辑)
这一级别将为 OTX 提供更灵活的约束条件。例如,output#0
的 capacity
必须大于 1000,或者要求一个输出的 data
字段必须等于它的 lock script 的哈希。
可组合性
OTX 协议的可组合性有两个要求,一个是不同的 OTX 之间的组合,另一个是 OTX 协议脚本呢和用户自定义脚本之间的组合。
多个 OTXs
在我们初始化一个 OTX 时,并不知道有多少个其他的 OTXs 会附加到这个 OTX 上来,也不知道这些 OTXs 的 lock scripts/type scripts 是什么。因此,不同的 OTXs 应该在相同的协议下保持可组合性。
这里的关键信息是,引用目标 cells 的签名规则必须使用一些相对索引,否则它们将在不同的栈序列中被打乱。
OTX 功能作为一个扩展
为了支持 OTX 功能,输入的 cells 的 lock script
需要遵守 OTX 规则。为了让整个事情变简单,并使得 OTX 之间能够实现互相兼容,最好创建一个通用的 OTX lib,然后每个 lock script
都调用它。
/* pseudo-code: OTX ready SECP256K1 lock script, note that the demo code cannot achieve scope level 3 (logic) constrans.*/
int main() { // load lock script args lock_args = load_script_args(CKB_SOURCE_GROUP_INPUT); // load OTX lib otx_handle = load_lib(OTX_LIB_HASH); // load OTX parameters in witness, which defines the signature coverage otx_parameters = load_witness(0, CKB_SOURCE_GROUP_INPUT); // hash the items defined in otx_parameters, // including all inputs with the same lock_script hash_init(digest); while (message = load_tx_input(CKB_SOURCE_GROUP_INPUT)) { hash_update(digest, message); } // extract items in TX TX = load_tx(); items = otx_handle.invoke(TX, otx_parameters); // hash the items for (item in items) { message = load_tx_item(item); hash_update(digest, message); } // load signature signature = load_witness(1, CKB_SOURCE_GROUP_INPUT); // recover pk pubkey = secp256k1_ecdsa_recover(digest, signature); // calculate pkhash pk_hash = hash(pubkey); // return 0 if pkhash matches lock_args return match(pk_hash, lock_args);}
回顾
OTX 必须识别约束条件,并且这里有四个等级的约束条件细分。为了使得约束条件不可变,我们需要对它们进行签名。我们还需要考虑 OTXs 的可组合性和代码的的可复用性,所以我们最好提供一个共享库来帮助锁定脚本获得 OTX 的功能。