Overview
在之前的 Open transaction prototype 中我们尝试了非常细粒度的 open transaction 设计,在这些设计中 open transaction 可能的形式繁多,要确保任意情况下多个 otx 组合的安全性变得很困难。同时我们也逐渐意识到,大部分需要 open transaction 的场景可能只会用到少数几种 otx pattern, 只要能满足这些场景的需求,降低 open transaction 组合的粒度,减少组合的灵活性,不仅能满足需要还能降低潜在的安全风险,反而是更好的设计选择。
基于这个思路,同时结合与 CoBuild protocol 兼容性的考量,我们设计了一个新的 Open transaction protocol, 本文将其命名为 CoBuild Open Transaction (OTX). CoBuild OTX 有如下特点:
- Otx 的结构基本与普通的 CKB Transaction 一致,也有 input / output cells, cell / header deps 以及 witness
- 每一个 Otx 的签名覆盖当前 Otx 包含的所有 input / output cells,cell / header deps 以及唯一的 witness 中,非签名部分的数据结构
- 无论 Otx 中有多少个不同的 lock script 出现,一个 Otx 都只有唯一的一个 witness 结构
- 一个 Otx 需要先被包含在一个完整的 CKB Transaction 中,之后再随着包含 Otx 的 CKB Transaction 一起打包上链。在完整的 CKB Transaction 中,一个 Otx 的 witness 所在位置,与当前 Otx input / output cells 在完整的 CKB Transaction 中所处位置之间,没有任何联系。
- Otx 内部的 input / output cells 之间的相对顺序不可调整,但是在一个包含多个 Otx 的完整 CKB Transaction 内部,多个 Otx 之间的相互顺序可以调整
- 在一个完整的 CKB Transaction 中,所有的 Otx 都只能连续出现,不能分散开来。换句话说,所有属于 Otx ,连续出现的 input / output cells,cell / header deps,witnesses 的内部,都不能有不属于某一个 Otx 的结构出现。
- 对于 CKB Transaction 中所有包含的 input / output cell,cell / header dep 这些结构来说,每一个结构都只能确定的属于某一个 Otx,或者是不属于任何 Otx。不可能存在某一个 cell,它的 lock script 按照 sighash 的模式运作,同时它的 type script 按照 otx 的模式运作。
Off-chain Construction
CoBuild OTX 的交易构建及签名过程与一个普通交易的构建及签名过程完全一致。具体可以参考 CKB Transaction CoBuild Protocol Overview 中的流程。除此之外,CoBuild OTX 还有搜集合并 OTX 为完整交易上链的流程,因此 CoBuild OTX 流程中会涉及两个参与方:
- Otx Creator:构建 Otx 并通过 p2p 网络或者其他方式传播 Otx 角色。期望某一个 Otx Agent 会搜集 Otx 并找到合适机会与其他匹配的 Otx 一起合并成完整交易上链。
- Otx Creator 可以是一个使用 CKB app 的普通用户,也可能是其他角色。比如对一个 dex 来说,可能会有做市商针对用户构建的 maker 端的 Otx 1(2, 3, …),相应的构建 taker 端的 Otx 100,并期望这里的 Otx 1, 2, 3, … 以及 Otx 100 可以合并交易上链。这里的做市商也是一个 Otx Creator。
- Otx Agent:搜集通过 p2p 网络或其他方式传播的 Otx,在 Otx 集合中寻找符合一定上链条件的一组 Otx,将这些 Otx 合并成一个 CKB Transaction 发送到 CKB 上链的角色。
- 合并上链的条件可以由用户通过 on-chain script 定制。比如可以是多个 Otx 满足彼此上链的条件,或者是多个 Otx 满足了作为一个 batch 提交从时间或空间上的的 threshold,etc.
- 在合并构建完整交易的过程中的最后一个 Otx Agent 可以用 SighashAll 模式的 WitnessLayout 添加由自己控制的 input cell(以及相应的 output cell),提供 CKB 上链手续费,并锁定 CKB Transaction 结构不能再发生变化。
CoBuild OTX 在链外使用如下的数据结构:
option BuildingPacketV1Opt (BuildingPacketV1);
table OtxBatchV1 {
txs: TransactionVec,
building_packet: BuildingPacketV1Opt,
script_infos: ScriptInfoVec,
}
union OtxBatch {
OtxBatchV1,
}
一个 Otx 是一个基本单位(最小粒度),来表述一个用户的一个操作。Otx 会分批打包上链,每一个上链的 CKB Transaction 都可能会包含多个相互关联的 Otxs。在上链之前,CoBuild OTX 用 OtxBatchV1
结构来表达有相互关联的多个 Otx。
Agent P2P Network
包含一个或多个 Otx 的 OtxBatchV1
结构,会在 Otx Agent 之间,通过 p2p 网络或其他通信渠道传播。如上图所示,Agent X 向 Agent Y 传递的 Batch 1 中,包含 Otx 1,Otx 2,Otx 3 这三个 Otx,以及这三个 Otx 所包含的 messages 中,引用到的两个 script info A 与 C。Script info 提供了解释这个 Batch 中包含的所有 Otx 的全部信息。
假设一个 Otx Creator 构建并签名确认了 Otx 4,Otx Creator 可以将包含 Otx 4 的 BuildingPacket 发送给 Agent Y,Agent Y 收到 BuildingPacket 后,可以抽取出 Otx 4,将 Otx 4 以及 BuildingPacket 中包含的 Otx 4 依赖的 script info A 与 B 合并到当前它所持有的 Batch 1 中,构造出包含 Otx 1,Otx 2,Otx 3,Otx 4 以及 script info A,B,C 的 Batch 2。此时 Agent Y 可以丢弃收到的 BuildingPacket(后续构建不需要 BuildingPacket 的 message 与 lock_actions),并将 Batch 2 在 Agent 之间广播。
在 Agent 网络中,可能有一个或者多个 Otx Agent 接收某种 Otx(例如上文中的 Otx 4)。Agent 网络可以分 topic 运作,Agents 可以订阅 topic, 某些 Agent 关心一类的 Otx,另外一些 Agent 关心其他类的 Otx.
假设在 Agent Y 广播 Batch 2 之后, Agent Z 收到了 Batch 2 ,并且发现可以把 Batch 2 中的 Otx 2 与 Otx 4 合并打包上链。Agent Z 可以从 Batch 2 中抽取出 Otx 2 与 Otx 4,并以这两个 Otx 为基础,构造一个完整的 CKB Transaction。
为了使 Otx 2 与 Otx 4 可以通过校验,Agent Z 有可能需要补足一些必要的 input / output cells(e.g. 提供 CKB 上链手续费),也可能需要补足完整交易所需的 cell deps,以及 witness。构造好完整交易之后,Agent Z 便可以把包含 Otx 2 与 Otx 4 的完整交易广播到 CKB 网络。
广播之后,Agent Z 需要更新本地的 Batch 2 信息,此时有两种选择:
- 构建 Batch 3:从 Batch 2 中移除掉 Otx 2 与 Otx 4,同时也移除掉不再需要的 script info A 与 B,然后把包含剩余 Otx 的 Batch 3 发送到 Agent P2P 网络中,希望后续的 Agent 以 Batch 3 为起点,进行后续的处理。
- 什么都不做:对于 P2P 网络中的其他 Agent 来说,他们也可以通过观测 CKB 网络,发现有包含 Otx 2 与 Otx 4 的完整交易在 CKB 打包上链,这样自然的,每一个 Agent 都需要从自己的 local 数据中,把 Otx 2 以及 Otx 4 相应移除
Side track: is papp an Otx Creator, or is papp an Otx Agent?
一个 papp 可能是 Otx Creator,也可能是 Otx Agent。我们以 orderbook DEX 中的一个做市商为例,取决于做市商的不同行为,可能有两种情况:
- 做市商可以持续观测某一个 Otx Agent 暴露的,当前的 Batch 中包含的用户挂单,并针对某一个或多个挂单,生成相应的 Otx 吃单。做市商选择把这样的 Otx 吃单发送给 Otx Agent,由 Otx Agent 完成最后的 CKB Transaction 构造上链。在这种设计下,我们认为做市商是 Otx Creator
- 注意一个吃单的操作,并不一定要实现为单独的一个 Otx。只要最后上链的 CKB Transaction 满足 CKB / UDT /的所有校验规则,我们完全可以用靠 SighashAll 模式解锁的 input / output cells 来实现对一个或多个 Otx 挂单的吃单操作。所以一个做市商也可以选择在 Agent P2P Network 中观察用户提交的挂单 Otx,当做市商打算针对某一些 Otx 吃单时,它可以针对这些 Otx 来构建完整的 CKB Transaction,通过 SighashAll 模式提供吃单部分的 CKBytes / UDT,并把构造完毕的 CKB Transaction 直接发送上链。在这种设计下,我们认为做市商是一个 Otx Agent
Otx construction in the context of other Otxs
在 Otx cobuild 过程中,可能会出现多个 Otx Co-Creator 需要在某一个已有的 Otx 的基础上,进行一个新的 Otx 构建的流程。一个例子是以多签 lock 持有的 Spore NFT,当有人通过一个 Otx 对这个 Spore NFT 进行出价时,多签的多个持有方,需要在 bid Otx 的背景下,进行多签 Otx 的构建。
如上图所示,这里可以利用包含 building_packet
字段的 Batch 结构来实现。多个 Otx Co-Creator 之间交换 Batch 结构,其中包含完整的包含 bid 请求的 Otx 1,Otx 1 所涉及的所有 script info A 与 C,以及一个完整的 BuildingPacket
结构。多个 Otx Co-Creator 可以按照已有的流程,通过 BuildingPacket
中的 lock_actions
交换签名信息,在收集到足够的签名后,可以把整体的 Batch 结构发送给一个 Otx Agent,Otx Agent 按照类似上述描述过的过程,从 Batch 中抽取包含 bid 请求的 Otx 1,再从 BuildingPacket
中抽取包含 accept order 动作的 Otx,将两个 Otx 都合并到自己的 Batch 中,在 Agent P2P 网络中继续传递,或是直接构造 CKB Transaction 上链。
On-chain Validation
基于 CKB Transaction CoBuild Protocol Overview 中的讨论,我们进行如下扩展:
// 一个完整的 CKB Transaction 中只能包含唯一的一个 OtxStart 结构,用于表示当前 CKB Transaction
// 中,连续出现的 Otx 的开始。OtxStart 中标记在 input / output cells 中,以及
// cell / header deps 中,第一个属于 Otx 的结构的位置。这里不需要标记 witness 的开始位置,
// 因为 OtxStart 本身可能就标记了 witness 中第一个属于 Otx 结构的开始位置。
//
// 紧接着 OtxStart 结构,witness 数据中会继续包含若干个 Otx 结构,每一个 Otx 结构都对应着
// 当前 CKB Transaction 中所包含的一个 OTx
//
// 取决于 OtxStart 在 witnesses 数组中具体所处的位置,OtxStart 结构本身,有可能并不被 CKB
// Transaction 中的任何一个签名覆盖。
table OtxStart {
start_input_cell: Uint32,
start_output_cell: Uint32,
start_cell_deps: Uint32,
start_header_deps: Uint32,
}
table SealPair {
script_hash: Byte32,
seal: ByteVec,
}
vector Seals <SealPair>;
// Otx 用于表示某一个确定的 Otx 结构,对于一个 Otx 来说,它有且仅有惟一一个以 Otx 结构呈现
// 的 witness。同时以 Otx 结构表示的 witness 在 CKB Transaction 中所处的位置,与该 Otx
// 实际包含的 input / output cells 在当前 CKB Transaction 中所处的位置之间没有任何确定关系
table Otx {
// 一个 Otx 可能比如有 3 个 input cells,这 3 个 input cells,并不都一定是相同的 lock
// 可能他们都是不同的 lock script,用不同的签名算法分别签名。
// 同时对一个 Otx 来说,只有 Otx 这一个 witness,
// 我们需要在这一个 witness 中,塞入多个不同 lock script 的不同签名。
// 因此这里用了 seals,这里 SealPair 的用法跟 Action 类似,
// 也是先用 script hash 匹配,匹配到再从 SealPair 中取出 Bytes 类型的 lock 字段
seals: Seals,
// input_cells 表示当前的 Otx 中,有几个 input cells,是一个数字。
// output_cells / cell_deps / header_deps 也类似
input_cells: Uint32,
output_cells: Uint32,
cell_deps: Uint32,
header_deps: Uint32,
message: Message,
}