例子:如何让 Spore 支持 CoBuild
我们可以以 Spore 为例,展示如何根据 CoBuild 规范为 Spore 添加对应的 Message 及交易结构。
Spore Message
针对 Spore,定义如下的 Message
schema:
// Common types are omitted here for simplicity
table Mint {
id: Byte32,
to: Address,
content_hash: Byte32,
}
option AddressOpt (Address);
table Transfer {
nft_id: Byte32,
from: AddressOpt,
to: AddressOpt,
}
table Melt {
id: Byte32,
}
union SporeAction {
Mint,
Transfer,
Melt,
}
Spore 对应的 ScriptInfo
,则定义如下:
name | Spore |
---|---|
url | |
script_hash | |
schema | |
message_type | SporeAction |
我们假设上述 ScriptInfo
结构经 molecule 序列化,再经过一次 ckb-hash 之后得到的结果为 <spore script info hash>
一个使用 JoyID lock 的 spore cell,数据结构如下:
Lock: JoyID lock
Type: Spore Type Script
Data: Spore Data
从 cell 来看,一个 Spore cell 的结构与未使用 CoBuild Message 时完全相同,没有改变。有所改变的是 Spore cell 所参与 transaction 中 witness 的内容。
Mint
Mint 操作如何实现,取决于 input cells 的 lock 的能力。有两种 tx 的构造方法:在 witness 中使用基于 WitnessArgs
的 legacy 结构,以及使用 WitnessLayout
结构的 cobuild 构造模式。
假设 lock 只支持基于
WitnessArgs
的老格式的话,可以构造如下的 transaction:
inputs:
input 0:
capacity: 1000 CKB
lock: JoyID lock A
type: <EMPTY>
data: <EMPTY>
input 1:
capacity: 500 CKB
lock: JoyID lock A
type: <EMPTY>
data: <EMPTY>
input 2:
capacity: 300 CKB
lock: JoyID lock A
type: <EMPTY>
data: <EMPTY>
outputs:
output 0:
capacity: 1600 CKB
lock: JoyID lock B
type: Spore type script
data: Spore data
output 1:
capacity: 199 CKB
lock: JoyID lock A
type: <EMPTY>
data: <EMPTY>
witnesses:
witness 0: WitnessArgs format
lock: Signature for JoyID lock A
input_type: <EMPTY>
output_type: <EMPTY>
witness 1: <EMPTY>
witness 2: <EMPTY>
witness 3: WitnessLayout format, SighashAll variant
seal: []
message: Message format
actions:
Action 0:
script_info_hash: <spore script info hash>
script_hash: <spore type script hash>
data: SporeAction format, Mint variant
id: <Spore id of output 0>
to: JoyID lock
content_hash: <hash of Spore data of output 0>
如果 input cells 使用的 lock 支持基于 CoBuild protocol,则可以使用如下结构的 transaction:
inputs:
input 0:
capacity: 1000 CKB
lock: JoyID lock A
type: <EMPTY>
data: <EMPTY>
input 1:
capacity: 500 CKB
lock: JoyID lock A
type: <EMPTY>
data: <EMPTY>
input 2:
capacity: 300 CKB
lock: JoyID lock A
type: <EMPTY>
data: <EMPTY>
outputs:
output 0:
capacity: 1600 CKB
lock: JoyID lock B
type: Spore type script
data: Spore data
output 1:
capacity: 199 CKB
lock: JoyID lock A
type: <EMPTY>
data: <EMPTY>
witnesses:
witness 0: WitnessLayout format, SighashAll variant
seal: Signature for JoyID lock A
message: Message format
actions:
Action 0:
script_info_hash: <spore script info hash>
script_hash: <spore type script hash>
data: SporeAction format, Mint variant
id: <Spore id of output 0>
to: JoyID lock
content_hash: <hash of Spore data of output 0>
假设当前交易为 <spore tx 1>
Spore dapp 实际会构造,并发送给 JoyID 端如下的数据结构:
BuildingPacket format, BuildingPacketV1 variant
message: Message format
actions:
Action 0:
script_info_hash: <spore script info hash>
script_hash: <spore type script hash>
data: SporeAction format, Mint variant
id: <Spore NFT id of output 0>
to: JoyID lock
content_hash: <hash of Spore NFT data of output 0>
payload: <spore tx 1 without witness 0>
script_infos: Array
0: <Spore ScriptInfo defined above>
lock_actions: <EMPTY>
注意我们这里为了说明完整,先给出了最后上链的 tx 结构,然后再给出 BuildingPacket
结构,但是在实际 UX 中,是 Spore dapp 先生成 BuildingPacket
结构,发送给 JoyID 钱包,由 JoyID 钱包呈现给用户,用户确认签名后,JoyID 端生成签名,把签名再返回给 Spore dapp,再由 Spore dapp 填入 tx 最后形成完整的上链 CKB transaction 的。也就是说是先有 BuildingPacket
(其中包含一个 witness 可能会调整,但是其他结构都已经确定好的 tx),后有完整 CKB transaction 的。
在 BuildingPacket
中,如下的数据需要在 UI 端呈现给最终的用户:
- 在
message
中包含的,对应当前 tx 的实际操作信息。在 Spore 的例子中,因为当前 tx 中只有一个 Spore type script,所以message
中包含的actions
数组只有一个对应 Spore 的 action。在未来更复杂的交易中(比如用 UDT 购买 Spore),message
中包含的actions
数组可能会包含多个实际的操作。 - 当前 tx 的手续费信息(通过
payload
中包含的 tx 结构导出) - 当前 tx 的 CKB transfer 信息(同样通过
payload
中包含的 tx 结构导出)
上面展示的两个 tx 实现了同样的 Mint 功能,只是在具体的 tx 结构上,有所不同。几个需要注意的点:
- Input cell 中包含的 lock script 使用 lock field(针对不同类型,从不同位置获取)中提供的签名校验整个交易的内容,对于不同的格式来说,签名使用的 message 计算方法略有不同:
- 在前一种基于
WitnessArgs
结构的交易中,使用传统的 sighash-all 模式计算签名的消息 - 在基于
WitnessLayout
的新格式的交易中,依照 CoBuild specification 来计算签名
- 在前一种基于
- Spore 可以选择 mint 给自己,这里例子中,选择从 A mint 给 B,主要是为了展示
SporeAction
中to
字段的用法。 - Spore type script 需要获取到
WitnessLayout
中包含的Message
格式的message
,并对其中的内容进行校验。确保Message
所包含的操作,与实际当前的 tx 的内容相符合。比如校验当前 tx 进行的的确是一个 mint 操作。
考虑历史兼容性问题,对 mint 操作来说可能会有从某些只支持基于 WitnessArgs
的旧版本交易格式 mint 出的 Spore。对下面其他以 Spore 为起点的操作来说,我们只考虑 lock 与 type 都支持基于 WitnessLayout
的新版本交易格式的情况。
Transfer
Transfer 交易:
inputs:
input 0:
capacity: 1600 CKB
lock: JoyID lock B
type: Spore type script
data: Spore 111
outputs:
output 0:
capacity: 1599.9 CKB
lock: JoyID lock C
type: Spore type script
data: Spore 111
witnesses:
witness 0: WitnessLayout format, SighashAll variant
seal: Signature for JoyID lock B
message: Message format
actions:
Action 0:
script_info_hash: <spore script info hash>
script_hash: <spore type script hash>
data: SporeAction format, Transfer variant
id: <ID of Spore 111>
from: JoyID lock B
to: JoyID lock C
假设当前交易为 <spore tx 2>
,Spore dapp 应该构造如下的数据结构,发送到 JoyID 端呈现并请求签名:
BuildingPacket format, BuildingPacketV1 variant
message: Message format
actions:
Action 0:
script_info_hash: <spore script info hash>
script_hash: <spore type script hash>
data: SporeAction format, Transfer variant
id: <ID of Spore NFT 111>
from: JoyID lock B
to: JoyID lock C
payload: <spore tx 2 without witness 0>
script_infos: Array
0: <Spore ScriptInfo defined above>
lock_actions: <EMPTY>
与 Mint 类似,这里 JoyID 应该向用户呈现三组信息:
- 在
message
中包含的,对应当前 tx 的实际操作信息。主要在data
字段中。 - 手续费信息(通过
payload
中数据计算) - CKB transfer 信息(同样通过
payload
中数据推导)
用户确认后,JoyID 可以依照 CoBuild spec,计算出 signing message hash 并进行签名。
当一个交易的 input cells 来源于两个 lock 时,则需要两个不同的签名覆盖:
inputs:
input 0:
capacity: 1600 CKB
lock: JoyID lock B
type: Spore NFT type script
data: Spore NFT 111
input 1:
capacity: 400 CKB
lock: JoyID lock B
type: <EMPTY>
data: <EMPTY>
input 2:
capacity: 300 CKB
lock: JoyID lock D
type: <EMPTY>
data: <EMPTY>
outputs:
output 0:
capacity: 1650 CKB
lock: JoyID lock C
type: Spore NFT type script
data: Spore NFT 111
output 1:
capacity: 349.9 CKB
lock: JoyID lock B
type: <EMPTY>
data: <EMPTY>
output 2:
capacity: 300 CKB
lock: JoyID lock D
type: <EMPTY>
data: <EMPTY>
witnesses:
witness 0: WitnessLayout format, SighashAll variant
seal: Signature for JoyID lock B
message: Message format
actions:
Action 0:
script_info_hash: <spore script info hash>
script_hash: <spore type script hash>
data: SporeAction format, Transfer variant
id: <ID of Spore NFT 111>
from: JoyID lock B
to: JoyID lock C
witness 1: <EMPTY>
witness 2: WitnessLayout format, SighashAllOnly variant
seal: Signature for JoyID lock D
一个 CoBuild Transaction 中只有唯一一个 witness 可以使用包含 Message
的 SighashAll 格式。对于有多个不同 lock 需要签名的交易来说,只有一个 lock 对应的 witness 可以用 SighashAll 格式包含 Message,其他 lock 对应的 witness 应该使用只包含签名的 SighashAllOnly 格式。
一个 CoBuild Transaction 中只会有一个 Message
结构,即使有多个 lock / type script,也应该把这些 lock / type scripts 需要的 actions 全部放在同一个 Message
结构中。
以上面的交易为例,交易中 input cell #0, #1 使用 JoyID lock B,而 input cell #2 使用 JoyID lock D。按照 CKB 的验证规则,我们需要在 witnesses 中提供 JoyID lock B 以及 JoyID lock D 的签名。在当前展示的例子中,我们选择把 Message
放在 JoyID lock B 对应的 witness 0 中,而 JoyID lock D 对应的 witness #2 使用了只包含签名的 SighashAllOnly 格式。
实际上在当前的交易中 JoyID lock B 与 D 签名时,使用的 signing message hash 完全相同。换句话说,JoyID lock D 签名时,依然需要覆盖存放在 JoyID lock B 对应 witness 中的 Message
信息。
假设当前交易为 <spore tx 3>
,Spore dapp 应该生成同样的 BuildingPacket
结构分别发送给 JoyID lock B 和 D 来签名:
BuildingPacket format, BuildingPacketV1 variant
message: Message format
actions:
Action 0:
script_info_hash: <spore script info hash>
script_hash: <spore type script hash>
data: SporeAction format, Transfer variant
id: <ID of Spore 111>
from: JoyID lock B
to: JoyID lock C
payload: <spore tx 3 without witness 0>
script_infos: Array
0: <Spore ScriptInfo defined above>
lock_actions: <EMPTY>
除此之外,因为使用 JoyID lock B 的 input cell 占据了 index #0, #1 的位置,witnesses 中 index #1 位置的 witness 就必须为空。对应 JoyID lock D 的 witness,应该放在 index #2 的位置。
Melt
Melt 交易:
inputs:
input 0:
capacity: 1600 CKB
lock: JoyID lock B
type: Spore type script
data: Spore 111
outputs:
output 0:
capacity: 1599.9 CKB
lock: JoyID lock C
type: <EMPTY>
data: <EMPTY>
witnesses:
witness 0: WitnessLayout format, SighashAll variant
lock: Signature for JoyID lock B
message: Message format
actions:
Action 0:
script_info_hash: <spore script info hash>
script_hash: <spore type script hash>
data: SporeAction format, Melt variant
id: <ID of Spore 111>
假设当前交易为 <spore tx 4> ,Spore dapp 应该构造如下的数据结构,发送给 JoyID 钱包呈现并请求签名:
BuildingPacket format, BuildingPacketV1 variant
message: Message format
actions:
Action 0:
script_info_hash: <spore script info hash>
script_hash: <spore type script hash>
data: SporeAction format, Melt variant
id: <ID of Spore NFT 111>
payload: <spore tx 4 without witness 0>
script_infos: Array
0: <Spore ScriptInfo defined above>
lock_actions: <EMPTY>