CKB Transaction CoBuild Protocol Overview

by Xuejie Xiao, Jan Xie

Thanks to Quake Wang, Ian Yang for PoCs and feedback.

中文版本

===== Table of Content =====

The construction of a CKB transaction involves the client processing user input, compute based on existing states, and generate new states. During the process a client needs to handle user operations, select transaction inputs, create outputs, set a reasonable fee etc. For a specific transaction, its construction, signing and broadcasting may occur on the same node or different nodes; the building process itself might involve multiple nodes working together. Therefore, CKB applications are characterized by states being generated off-chain and verified on-chain; CKB clients (e.g. wallets) are deeply involved in application transaction processing. The collaborative building process of CKB Transaction is essentially the execution process of CKB applications.

The CKB Transaction CoBuild Protocol describes an off-chain procedure for multiple parties to collaboratively create a CKB Transaction. This includes standard procedures related to transaction building (Building) and signing (Signing), as well as data exchange formats. We hope that by proposing and defining such a protocol we can lower the development barrier for CKB applications, enhance their composability with each other, improve user experience within these apps, facilitate decentralization among roles in these apps, and make it easier for those roles to collaborate.

This article will outline the objectives and design of the Transaction CoBuild Protocol, and provide specific examples for interested readers. The design and implementation of the CoBuild protocol is still in progress, we are working with community teams to build PoC and collect feedback.

Goals

The design goals of the CKB Transaction CoBuild Protocol include:

  • Message signing: Building a user-friendly signing standard (similar to EIP712), where users can know what action they are confirming, wallets can ensure that the message presented for user signing is consistent with the underlying transaction, and on-chain scripts can verify that the message signed by the user matches the transaction effects.
  • Script interface: Constructing a friendly on-chain script interface standard. A type/lock script is similar to a solidity contract on Ethereum, which can define multiple actions (functions) that make up the interface of the script. When application developers interact with a CKB app, they should be able to obtain its scripts and corresponding actions through ScriptInfo, then construct Message. Given these standard procedures and data structures, automated tools can be created. Eventually developers should be able to handle Message without dealing with transaction data themselves, given the help of automated toolchain. This way, most of the time developers only need to deal with ScriptInfo / actions / Message, have an experience similar to handling solidity ABI / message.
  • Witness Layout: Creating a composable and automation-friendly transaction witness layout standard, replacing existing WitnessArgs.
  • Transaction Building: Providing standard procedures and data exchange format for collaborative transaction building, for both full and open transactions.
  • P2P based, Local first: All roles in all processes should be freely joinable and selectable without permission; specific participants may take on one or more roles. We hope this will build a p2p paradigm-based perpetual app network. (Proxy or server/client mode could solve compatibility issues in certain special scenarios but shouldn’t be included in standard procedures.)

Roles

The process of collaborative transaction building involves the following roles:

  • Builder: Provides the basic data needed for a transaction, such as inputs, outputs, messages or witnesse data (exclude signatures).
  • Asset Manager: Has an understanding of assets on CKB (e.g. CKByte, xUDT, Spore), can query and display assets, provides corresponding asset operations.
  • Fee Manager: Analyzes the on-chain fee and feerate in real time, set fee/feerate for given transactions, displays fee/feerate to users;
  • Signer: Keeps and manages keypairs, signs data (CKB transactions or any message) upon user request.

A node in this process can take on one or more roles.

In the following chapters we will outline CoBuild protocol based on the most common scenario where CKB app takes on the role of Builder while Wallet assumes roles of Asset Manager, Fee Master and Signer. The interaction between CKB app, Wallet and User follows standard procedures.

More complex situations, like multiple builders or different participants being asset manager, fee manager, signer respectively, need to be discussed according to specific scenarios which is not covered in this article.

Basic Flow

During the use of the CKB app by users:

  1. The user clicks a button in the CKB app to initiate an operation, and the app generates a Message based on the user’s requested operation;
  2. The app (as Builder) processes the Message according to its own application logic to get execution results, uses these data to build corresponding Transaction, reflecting user actions (Message) and corresponding execution results (inputs/outputs). The relevant WitnessLayout data (which includes Message) is placed in Transaction witness.
  3. The app builds a BuildingPacket, puts the Transaction into BuildingPacket.payload, puts the ScriptInfo that Message depends on into BuildingPacket.script_infos, puts Message into BuildingPacket.message, and puts cell data corresponding to Transaction.inputs into BuildingPacket.resolved_inputs;
  4. The app (as Fee Manager) calculates and sets transaction fees, updates Transaction. If fees can be adjusted by wallet, then set BuildingPacket.change_output will specify output for adjusting fee purposes.
  5. The App sends BuildingPacket to Wallet(as Signer), requesting Wallet for signing;
  6. Wallet parses BuildingPacket, presents Message, payload, and script_infos for user confirmation:
    a. If User agrees: sign BuildingPacket.payload; fill signature into transaction witness according to WitnessLayout rules; broadcast fully signed transactions.
    b. If User refuses: notify CKB app that signing request has been rejected.

Flowchart of Transaction Building & Signing

Data Schema

The core data structure used in the transaction construction process is BuildingPacket, and the core data structure handled by on-chain contracts is WitnessLayout. These two structures are linked by Message.

The definitions of these data structures are as follows:

// CKB CoBuild Data Structures
//
// WitnessLayout and BuildingPacket are two entry points: 
// - WitnessLayout is the entry point for constructing/parsing witness in on-chain scripts
// - BuildingPacket is the standard format for data exchange among parties during Transaction Co-build process
// Considering possible future upgrades, all entry structures are unions that can be extended by adding variants.

array Byte32 [byte; 32];
array Hash [byte; 32];
vector ByteVec <byte>;
vector String <byte>; // for UTF-8 encoded bytes

// A (script_info, script, action) combo
// Represents the specific action that the user wants to execute: action Y in script X of app XXX
table Action {
  script_info_hash: Byte32,   // script info
  script_hash: Byte32,        // script
  data: ByteVec,              // action data
}
vector ActionVec <Action>;

table Message {
  actions: ActionVec,
}

table ScriptInfo {
  // The dapp name and domain the script belongs to
  name: String,
  url: String,
  
  // Script info.
  // schema: script action schema
  // message_type: the entry action type used in WitnessLayout
  script_hash: Byte32,
  schema: String,
  message_type: String,
}
vector ScriptInfoVec <ScriptInfo>,

table ResolvedInputs {
  outputs: CellOutputVec,
  outputs_data: BytesVec,
}

table BuildingPacketV1 {
  // Represents user operations, contains actions
  message: Message,

  // standard CKB transaction: <https://github.com/nervosnetwork/ckb/blob/3d674d558e5574f0c77a52798775c903561a933a/util/gen-types/schemas/blockchain.mol#L66>
  // Contains the `Message`-corresponding transaction data required by wallet.
  // `Message` contains actions initiated by users, these actions will be understood and transformed into corresponding transactions (i.e., state transitions corresponding to Message) by builder and filled into BuildingPacket.payload.
  payload: Transaction,

  // resolved_inputs will store the cell data corresponding to payload transaction inputs.
  // The wallet can use this information to calculate and display the value and fees of CKByte transfer.
  resolved_inputs: ResolvedInputs,

  // Represents which output of the payload transaction is change, optional.
  // If empty, it means that the fee/feerate of this transaction is not adjustable, user must use specified fee/feerate.
  // If not empty, it indicates that the wallet can modify the capacity of the change output to adjusting tx fee/feerate.
  change_output: Uint32Opt

  // Script(info)s used by actions in message.
  script_infos: ScriptInfoVec,

  // A transaction may use multiple lock scripts, each of which may need to exchange some temporary data during the signing process (e.g. multisig).
  // Those temp data can be saved in this field. This field does not need to be signed and will not be included in the final transaction.
  lock_actions: ActionVec,
}

union BuildingPacket {
  BuildingPacketV1,
}

table SighashAll {
  // The intent of the seal is to store the "final seal" (i.e. user signature) used to finalize other parts of the transaction.
  // The seal itself can be modified by others after the transaction broadcast (malleable), and parts other than the seal are protected by it and unmodifiable (non-malleable).
  // This dedicated field helps separate data that needs to be finalized by signatures from signatures themselves, making transaction data processing easier.
  seal: ByteVec,
  message: Message,
}

table SighashAllOnly {
  seal: ByteVec,
}

// placeholder for open transactions
table OtxStart {...}
table Otx {...}

union WitnessLayout {
  SighashAll: 4278190081,
  SighashAllOnly: 4278190082,
  Otx: 4278190083,
  OtxStart: 4278190084,
}

Examples

Nervos DAO

With CoBuild we can now build an independent web UX for Nervos DAO! The Nervos DAO type script has been running on mainnet for a while. In the past, only a few applications like Neuron and CKBull provide built-in access to Nervos DAO. Now we can easily build a web UX for it, as demonstrated by this PoC. Not only that, but we can also integrate the Nervos DAO app with JoyID, Unisat, OKX wallet or other wallets to make it more accessible for users.

If you’d like to dive in please check the code and documentation in this repository.

Spore

Example: How To Improve Spore with CoBuild

Appendix

update 2024/02/16: added “Table of Content” and 4 appendixes: “CoBuild Hashes”, “Lock/Type Script Validation Rules”, “Custom Union Ids in WitnessLayout”, “CoBuild-Only or CoBuild+Legacy”.

update 2024/02/14: added Appendix and a link to Open Transaction Cobuild

update 2024/02/11: added Example: How To Improve Spore with CoBuild.

update 2024/03/15: updated lock script validation rule, now an Action object can use lock script hash as its script_hash value.

11 Likes

by Xuejie Xiao, Jan Xie

Thanks to Quake Wang, Ian Yang for PoCs and feedback.

===== 目录 =====

CKB Transaction 是在链外构建并签名的,Transaction 的构建过程即客户端处理用户输入并生成计算结果的过程,涉及用户操作,inputs 的选择,outputs 的生成,手续费设置等方面。对于一个特定的 Transaction,它的构建、签名和广播可能发生在同一个节点上,也可能在不同节点上,构建过程本身可能是多个节点合作参与的。因此,CKB 应用的特点是状态在链外生成,链上验证,客户端需要深度参与应用的交易处理,CKB Transaction 的协同创作过程即 CKB 应用的执行过程。

CKB Transaction CoBuild Protocol 描述的是一个多个参与方之间协同创作 CKB Transaction 的链外流程,包含与交易构建 (Building) 和签名 (Signing) 相关的标准流程和数据交换格式。我们希望通过提出和定义这样一个标准流程,降低 CKB 应用的开发门槛,提高 CKB 应用之间的可组合性,提高应用的用户体验,促进 CKB 应用角色的去中心化,使得 CKB 应用参与角色可以更容易的相互协作。

这篇文章将概述 Transaction CoBuild Protocol 的目标,角色和思路,并给出具体的例子供参考。CoBuild protocol 的设计和实现还在进行中,我们正在与社区团队一起构建 PoC 和搜集反馈。

Goals

CKB Transaction CoBuild Protocol 的设计目标包括:

  • Message signing: 构建一个对终端用户友好的签名标准(类似 EIP712),用户可以知道自己确认的是什么动作,钱包可以确认呈现给用户签名的 message 与交易数据一致,script 可以验证用户签名的消息与交易行为一致。
  • Script interface: 构建一个友好的 on-chain script 调用标准。一个 type/lock script 类似 Ethereum 上一个 solidity 合约,可以定义多个 actions (functions), 这些 actions 构成了 script 的接口。应用开发者在和一个 CKB app 做交互的时候,应该可以通过 ScriptInfo 拿到它的 scripts 和对应的 actions, 然后构造 Message. 只要有了 Message, 后续的签名和inputs/outputs都有标准流程/自动化工具可以实现,开发者只需要处理 Message, 不需要自己处理交易数据。这样开发者只需要和 ScriptInfo / actions / Message 打交道,可以获得与处理 solidity ABI / message 类似的体验。
  • Witness Layout: 构建一个方便组合、方便工具自动编排的 Witness layout 标准,代替现有的 WitnessArgs.
  • Transaction Building: 提供链外合作构建 Full/Open Transaction 的标准流程及据交换格式。
  • P2P based, Local first: 所有流程中所有参与角色应该是无需许可可以自由加入自由选择的,一个具体的参与者可以承担一个或者多个角色。我们希望由此构建一个基于 p2p 范式的 perpetual app network. (proxy 或者 server/client 模式可以用来解决某些特殊场景的兼容性,但不应该放到标准流程中。)

Roles

协同创作交易的过程中涉及如下角色:

  • Builder: 提供完整 transaction 所需要的素材,例如 inputs, outputs, messages 或者除了签名之外的 witnesse 数据等。
  • Asset Manager: 理解 CKB 上的资产(e.g. CKByte, xUDT, Spore),能够查询并展示资产,提供相应的资产操作。
  • Fee Manager:分析给定交易支付的 fee 及 feerate,展示 fee/feerate 给用户,根据 CKB 交易池状况设置合理手续费;
  • Signer: 管理 keypairs, 根据用户请求对数据(CKB 交易或者是任意 message)进行签名;

参与方可以承担一个或者多个角色。

接下来我们会基于最常见的场景概述 CoBuild protocol,其中 CKB 应用承担 Builder 角色,Wallet 承担 Asset Manager, Fee Master 以及 Signer 的角色,CKB 应用, Wallet, 用户按照标准流程进行交互。

更复杂的情况,例如有多个 builder, 或者 asset manager, fee manager ,signer 分别是不同参与方,需要根据具体场景具体讨论,本文不做深入。

Basic Flow

用户在使用 CKB app 的过程中

  1. 点击 app 某个按钮发起操作,app 根据用户请求的操作,生成 Message;
  2. app (as Builder) 根据自己的应用逻辑,处理 Message 得到执行结果,用这些数据构建对应的 Transaction, 体现用户动作 (Message) 和对应的执行结果 (inputs/outputs). Transaction witness 中放置相应的 WitnessLayout 数据(其中包含 Message)。
  3. app 构建 BuildingPacket, 将 Transaction 放入 BuildingPacket.payload 中,将 Message 依赖的 ScriptInfo放入 BuildingPacket.script_infos 中,将 Message 放入 BuildingPacket.message 中, 将 Transaction inputs对应的 cell 内容放入到BuildingPacket.resolved_inputs中;
  4. app (as Fee Manager) 计算并为交易设置手续费,更新 Transaction,如果手续费可以允许 wallet 进行调整, 那么设置 BuildingPacket.change_output将对应的 output 指定为用于调整手续费用途
  5. app 将 BuildingPacket 发送给 wallet (as Signer), 请求 wallet 进行签名;
  6. wallet 解析 BuildingPacket, 将 Message, payload, 以及 script_infos 呈现给用户确认:
    a. 如果用户同意,对 BuildingPacket.payload 中的交易数据进行签名,根据 WitnessLayout 规则将签名填入交易 witness 中,将完整的签名后交易广播。
    b. 如果用户拒绝,通知 CKB App 签名请求被拒绝。

Transaction Building & Signing 流程图

Data Schema

交易构建流程中使用的核心数据结构是 BuildingPacket, 链上合约处理的核心数据结构是 WitnessLayout,这两个结构通过 Message 关联起来。

其中数据结构定义如下:

// 这是一个汇总所有 CoBuild 数据结构的 Schema
//
// WitnessLayout, BuildingPacket 是两个入口:
// - WitnessLayout 是 on-chain scripts 构造/解析 witness 入口
// - BuildingPacket 是 Transaction Co-build 流程中各方交换数据的标准格式
// 考虑到未来可能的升级需要,入口结构都是 union,可以通过增加 variant 进行扩展。

array Byte32 [byte; 32];
array Hash [byte; 32];
vector ByteVec <byte>;
vector String <byte>; // for UTF-8 encoded bytes

// A (script_info, script, action) combo
// 代表用户想要执行的具体的动作: app XXX 中 X script 的 action Y
table Action {
  script_info_hash: Byte32,   // script info
  script_hash: Byte32,        // script
  data: ByteVec,              // action data
}
vector ActionVec <Action>;

table Message {
  actions: ActionVec,
}

table ScriptInfo {
  // The dapp name and domain the script belongs to
  name: String,
  url: String,
  
  // Script info.
  // schema: script action schema
  // message_type: the entry action type used in WitnessLayout
  script_hash: Byte32,
  schema: String,
  message_type: String,
}
vector ScriptInfoVec <ScriptInfo>,

table ResolvedInputs {
outputs: CellOutputVec,
outputs_data: BytesVec,
}

table BuildingPacketV1 {
  // 表示用户操作,包含 actions
  message: Message,

  // standard CKB transaction: <https://github.com/nervosnetwork/ckb/blob/3d674d558e5574f0c77a52798775c903561a933a/util/gen-types/schemas/blockchain.mol#L66>
  // 包含钱包完成功能所需要的、与 Message 对应的交易信息。
  // `Message` 包含的是用户主动发起的动作信息,这些动作会被 builder 理解并转化为对应的
  // 交易(也就是对应 Message 的 state transition)填入到 BuildingPacket.payload 中。		
  payload: Transaction,

  // resolved_inputs 会存有 payload transaction inputs 所对应的 cells 信息
  // 钱包可以用这个信息来计算和显示 ckb transfer 的数值和手续费
  resolved_inputs: ResolvedInputs,

  // 代表 payload transaction 哪个 output 是找零, 可选项
  // 如果为空, 则代表当前这个 transaction 无法调整找零, 相当于只能使用指定的手续费
  // 如果不为空, 则代表钱包可以修改对应 output 上的 capacity, 来实现调整手续费的目的
  change_output: Uint32Opt

  // Message 中 actions 依赖的所有 ScriptInfo
  script_infos: ScriptInfoVec,

  // 一个交易可能会用到多个不同的 lock script,每个 lock script 在 Signing 过程中,
  // 都可能会需要交换一些临时数据,这些数据可以保存到这里。这些数据不需要签名也不会被包含到最终交易。
  lock_actions: ActionVec,
}

union BuildingPacket {
  BuildingPacketV1,
}

table SighashAll {
  // seal 的设计意图是用来存放用来 cover 交易中其他部分的“最终的封印”,e.g. signature
  // seal 本身在交易广播后是有可能被其他人修改的 (malleable),除 seal 外的其他部分受到 seal 保护是不可修改的(non-malleable)。
  // 通过专用的字段 seal 有助于把 witnesses 里面需要被 signature 保护的数据和 signature 自身隔开,方便代码处理
  seal: ByteVec,
  message: Message,
}

table SighashAllOnly {
  seal: ByteVec,
}

// placeholder for open transactions
table OtxStart {...}
table Otx {...}

union WitnessLayout {
  SighashAll: 4278190081,
  SighashAllOnly: 4278190082,
  Otx: 4278190083,
  OtxStart: 4278190084,
}

Examples

Nervos DAO

有了 CoBuild 的帮助,我们就可以为 Nervos DAO 构建独立的 web UX 了!Nervos DAO type script 早就在 mainnet 上运行,过去只有 Neuron 和 CKBull 少数几个应用内置了对 Nervos DAO 的支持,现在我们可以很轻松的为它构建 web UX,正如这个 PoC 所展示的。不仅如此,我们还可以将 Nervos DAO app 与 JoyID, Unisat, OKX wallet 或者其他 wallet 集成起来,方便用户使用。

如果想要了解更多细节,请翻阅这个仓库中的代码和文档。

Spore

例子:如何让 Spore 支持 CoBuild

附录

2024/02/16 更新:增加了目录以及四个附录:“CoBuild Hashes”, “Lock/Type Script Validation Rules”, “Custom Union Ids in WitnessLayout”, “CoBuild-Only or CoBuild+Legacy”.

2024/02/14 更新:增加了 附录 以及 CKB CoBuild Open Transaction (OTX) Overview.

2024/02/11 更新:增加了 例子:如何让 Spore 支持 CoBuild.

2024/03/15 更新:调整了 lock script 校验规则,Action 也可以使用 lock script hash 作为 script_hash 项的值

7 Likes

Example: How to Improve Spore with CoBuild

中文版

Here we use Spore as an example to demonstrate how to add corresponding Message and other CoBuild data structures for Spore according to the CoBuild specifications.

Spore Message

For Spore, we can define the following 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,
}

and ScriptInfo like this:

name Spore
url
script_hash
schema
message_type SporeAction

The above ScriptInfo structure will be serialized by molecule, and hashed by ckb-hash, let’s call the obtained result <spore script info hash>.

A spore cell using JoyID lock has the following data structure:

Lock: JoyID lock
Type: Spore Type Script
Data: Spore Data

The structure of a Spore cell remains completely unchanged as when not using CoBuild Message. What changed is the witness in transactions involving Spore cells.

Mint

How Mint operation is implemented depends on the capability of input cells’ locks. There are two methods to build a Spore mint transaction: using legacy structure based on WitnessArgs, and using WitnessLayout structure and CoBuild flow.

Assuming that lock only supports the legacy format WitnessArgs, a mint transaction can be constructed as follows:

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>

If the lock used by input cells supports CoBuild, a transaction with the following structure can be used:

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>

Assume that the current transaction is <spore tx 1>. Spore dapp can build and send the following packet to 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>

Here we first present the mint transaction structure, then give the BuildingPacket structure. But in actual flow, it’s the Spore app generating BuildingPacket first, then sending it to JoyID wallet which presents it to user. After user confirms and signs, JoyID generates signature and sends it back to the Spore app which fills received siganture in, complete and finalize the minting transaction. There’s a BuildingPacket before the finalized minting transaction.

The following data in BuildingPacket should be shown to end users by wallet:

  • The actions in message. In this Spore example since there’s only one spore type script in transaction so actions has only one member. For future more complex transactions (like buying spores with UDT), actions may contain many members.
  • The transaction fee/feerate
  • CKByte transfer info

The above two transaction examples implement the same Mint function. Some notes:

  • Lock scripts in input cells use signatures in lock fields (for different type scripts, get from different locations in witnesses) to verify the transaction. In the two examples the methods of calculating signing message are slightly different:
    • In the transaction using WitnessArgs, the classical sighash-all mode is used to calculate signing message;
    • In the transaction using WitnessLayout, it follows CoBuild specification to calculate signing message.
  • Someone can mint a Spore for himself. The A minting for B example demonstrates the usage of ‘to’ field within ‘SporeAction’.
  • Spore type script should extract ‘message’ from WitnessLayout and verify it matches the transaction data, e.g. verify if the state transition presented by the transaction is a Spore mint.

For minting there might be cases in which transactions with the old WitnessArgs are used, and we use minting as an example to show how to be backward compatible with two different transaction structure. For other Spore actions we suppose both lock and type script used support new version transaction format based on WitnessLayout.

Transfer

Transfer transaction should be like:

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

Assuming the current transaction is <spore tx 2>, a Spore dapp should construct the following data, and send it to JoyID for display and request signature:

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>

Similar to Mint, here JoyID should present three sets of information to users:

  • The information in message (especially in message.data) which reprensets the actual operations will be carried out by this transaction.
  • Transaction fee/feerate (calculated from data in payload).
  • CKBytes transfer (also derived from data in payload).

After user confirmation, JoyID can generate signing message hash according to CoBuild spec and sign.

If a transaction’s input cells use multiple (e.g. two) locks, more signatures are required:

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

In a CoBuild transaction, only one witness can use SighashAll format containing Message. For transactions that require signatures from multiple different locks, only one lock’s corresponding witness can use SighashAll format with Message included; other lock’s corresponding witnesses should use SighashAllOnly format that contains only signature.

There will be only one ‘Message’ in a CoBuild transaction. Even if there are multiple locks / type scripts, all actions required by these locks / type scripts should be placed within this same ‘Message’.

Taking above transaction as an example: input cell #0 and #1 uses JoyID lock B while input cell #2 uses JoyID lock D. According to CKB verification rules we need provide signatures for both JoyID lock B and D within witnesses. Here we choose put ‘Message’ into witness 0 which corresponds with JoyID Lock B, whereas witness #2 which corresponds with JoyID Lock D used SighashAllOnly format.

Actually when signing using JoyID Locks B & D they’re using exactly same signing message hash. In other words when JoyID Lock D signs it still needs to cover ‘Message’ stored in witness corresponding with JoyID Lock B.

Assuming the current transaction is <spore tx 3>, a Spore dapp should generate same BuildingPacket and send separately to JoyID lock B and D for signature:

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>

In addition, because input cells with JoyID lock B use witnesses at index #0, #1, witness at index #1 must be empty. Witness corresponding with JoyID lock D should be placed at index #2.

Melt

Melt transaction:

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>

Assuming the current transaction is <spore tx 4>, a Spore dapp should construct the following data, send it to JoyID wallet for display and request a signature:

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>
3 Likes

例子:如何让 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,主要是为了展示 SporeActionto 字段的用法。
  • 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>
2 Likes

Appendix: CoBuild Hashes

中文版本

This section provides a detailed introduction to the various hashing algorithms used in the CoBuild protocol.

Signing Message Hash

The term signing message hash is used here to represent the data being verified against the signature.

The implementation of Lock needs to consider the impact of wallet. Since CKB has the ability to implement other chains’ transaction signing algorithms, CKB users may use wallets of other chains to operate assets on CKB. In such scenarios, a lock needs to consider how signing messages are calculated in non-CKB-specific wallet’s signing processes. As we cannot modify these non-CKB-specific wallet’s signing message calculation and they usually differ, there can’t be a unified algorithm and process for calculating signing mesage hash in CKB lock script. Therefore, CoBuild protocol should only specify relevant data in CKB transactions that need coverage within signing message hash. A CoBuild-compatible lock script just needs ensure these necessary data are covered by signing message hash for security assurance.

When calculating signing message hash, a lock should first extract ‘Message’ based on whether it is under SighashAll or Otx mode. Under all circumstances, ‘Message’ must be covered by ‘signing message hash’. The lock script also needs acquire some other transaction-related data such as current tx hash and certain cell content etc., then based on its mode it concatenates contents stipulated by CoBuild protocol, with information required by its own logic; after one or multiple cryptographic hashing it gets the ‘signing message hash’ for verification against the signature.

Although there isn’t mandatory ordering requirement for contents like ‘Message’, tx hash and cell data that need coverage of signing message hash, different transactions should provide different preimage data for hashing function whenever possible.

For example: suppose a CoBuild-compatible lock’s signing message hash covers ‘Message’ and certain cell data (note that in actual scenarios, data needing coverage by signing message hash is not limited to these, this is just a simple example), the lock chooses to concatenate ‘Message’ with cell data directly into a string then performs one hashing operation to obtain signing message hash. Then for following two situations, the generated signing message hash would be identical:

  • Message being 0xaabbccddaabbccdd, while cell data being 0x10101010.
  • Message being 0xaabbccdd, while cell data being 0xaabbccdd10101010.

This algorithm obviously isn’t good enough as it allows possible transaction forgery since the same signing message preimages/hashes are constructed from different transactions and different Messages.

To avoid such problem, a common practice is for lock script to concatenate length of a piece of data at its front. For instance in above example, lengths of cell data are either 4 or 8. If we also include length information into hashing then these two cases will generate distinct signing messages hashes eventually.

Also note: Molecule-format data structures which possess canonical encoding property are widely used in CoBuild process. Simply put, a Molecule-format data structure either has fixed length or like Message where first four bytes already contains entire structure’s length. Therefore there’s no need manually prepend lengths onto such structures expressed in molecule format to avoid aforementioned issue.

SighashAll Variant

In the SighashAll mode, the signing message hash should cover the following content:

  • The Message contained in the only SighashAll in the current transaction. Since Message is in molecule format, there’ no need to include the length of the Message part in hashing.
  • The tx hash that can be obtained through CKB syscall. Note that tx hash is always 32 bytes and does not require additional length concatenation.
  • All input cells and data obtainable via CKB_syscall. As tx hash has covered inputs quantity, there’s no need for further inputs count concatenation here. Input cell is presentedy as CellOutput in molecule format thus requires no additional length prefix. However, data is in variable-length raw format, to ensure safety it’s necessary to prepend the length of data.
  • The lengths and contents of all unmatched witnesses that exceed the number of input cells.

When calculating signing message hash, a lock can concatenate all these contents together in a certain order, and further concatenate extra data needed by itself. Then the lock gets the signing message hash by feeding the concatenated string into a cryptographic hash function.

Note that the above hashing algorithm is just an example. In fact, CoBuild only requires lock script to ensure that certain data are covered by cryptographic hash. As long as it’s defined clearly, any other hashing algorithm can be used and achieve the same security.

For instance, for some (perhaps with special requirements) lock scripts, you could use the following algorithm:

  • First concatenate tx hash, all input cells and data along with lengths and content of all unmatched witnesses together; run a cryptographic hash function on this concatenated string and get a hash, let’s call it skeleton hash.
  • Next, the lock script concatenates a fixed prefix with Message structure and skeleton hash, run another hash function and get another hash as signing message hash.

Take the transaction below as an example:

A CoBuild-compatible lock script should cover the following content in its signing message hash during verification:

  • The Message contained in the transaction’s SighashAll type witness
  • Current tx Hash
  • CellOutput corresponding to I1 serialized in molecule format
  • CellOutput Data corresponding to I1, with its length and actual content represented in little-endian encoding u32
  • CellOutput corresponding to I2 serialized in molecule format
  • CellOutput Data corresponding to I2, with its length and actual content represented in little-endian encoding u32
  • Length of Witness 3 and its actual content represented in little-endian encoding u32
  • Length of Witness 4 and its actual content represented in little-endian encoding u32
  • Length of Witness 5 and its actual content represented in little-endian encoding u32

SighashAllOnlyVariant

In addition to the SighashAll variant, there is a special SighashAllOnly variant of WitnessLayout. This variant only contains seal, not message. If all witnesses in a transaction are of the SighashAllOnly type, it means that this transaction does not contain any Message.

In this case, when a lock script verifies signatures, the following content should be covered in the signing message hash:

  • The tx hash obtained from CKB syscall.
  • All input cells and data obtained from CKB_syscall. Since tx hash has already covered inputs count, there’s no need to include them again here.
    -The length and content of all unmatched witnesses exceeding input cell count.

If a lock script needs to support both modes with or without ‘Message’, different prefixes should be added or the built-in methods of hash algorithm (such as personalization provided by blake2b) should be used to distinguish transactions with ‘Message’ and without ‘Message’, so as to avoid generating the same signing message hash for these two different types of transactions.

Otx Variant

In Otx mode, the signing message hash should cover:

  • The Message contained in the witness corresponding to current Otx.
  • The number of input cells covered by Otx, all content of input cells in CellInput format, including cell points in CellOutput format, and cell data in raw data form.
  • The number of output cells covered by Otx and all content of output cells including cell points in CellOutput format and cell data in raw data form.
  • The number of cell deps covered by Otx and all content of cell deps in CellDep format.
  • The number header deps covered by OTX and all contents represented using Byte32.

Note here cell data exists as variable-length raw strings. To ensure safety, a lock needs to prepend length before cell data. Besides this, other included structures are in molecule format which do not require additional length concatenation.

When calculating signing message hash, a lock can concatenate these contents together according to a certain order, and any extra data required by itself can also be added. Then the lock calculates get the result hash by feeding the concatenated string into a cryptographic hash function.

For security reasons it’s recommended to distinguish signing preimages/hashes in SighashAll mode & Otx mode by using different prefixes or built-in methods from hashing functions (like Blake2B’s personalization).

More specifically here we suggest using different prefixes for the following three scenarios:

  • SighashAll mode transaction (with Message)
  • SighashAllOnly mode transaction (without Message)
  • Otx mode transaction

Take the transaction below as example:

When a CoBuild-compatible lock script is verifying the transaction, the following content should be covered in calculated signing message hash:

  • The unique Message contained in the Otx typed witness of the open transaction.
  • 2 (the number of input cells as little endian encoded u32)
  • I1’s CellInput
  • I1’s CellOutput Data in molecule format, including data length and actual content as u32 in little-endian encoding
  • I2’s CellInput
  • I2’s CellOutput Data in molecule format, including data length and actual content as u32 in little-endian encoding
  • 3 (the number of output cells, as u32 in little endian encoding)
  • O1’s CellOutput
  • Length of O1 cell data
  • Actual content of O1 cell data
  • O2’s CellOutput
  • Length of O2 cell data
  • Actual content of O2 cell data
  • O3’s CellOutput
  • Length of O3 cell data
  • Actual content of O3 cell data
  • ‘1’ (the number of cell deps as u32 in little endian encoding)
  • D1’s CellDep
  • 4 (the number of header deps as u32 in little endian encoding)
  • H1 as Byte32
  • H2 as Byte32
  • H3 as Byte32
  • H4 as Byte32

Example

This repository provides an example implementation of signing message hash.

This example shows how to concatenate the data required by the CoBuild specification and obtain the result signing message hash through a blake2b hash calculation in three different modes.

For three different modes in the example, we have chosen different blake2b personalizations to avoid mistakenly generating the same hash for different types of transactions:

  • For SighashAll mode, with Message in transaction: use ckb-tcob-sighash
  • For SighashAllOnly mode, without Message in transaction: use ckb-tcob-sgohash (‘sgo’ means ‘sighash only’)
  • For Otx mode: use ckb-tcob-otxhash

As mentioned above, this is just one example implementation of a signing message hash. For a lock script to be CoBuild compatible and hashing securely, it should make sure that

  1. all data required by the CoBuild protocol are correctly covered,
  2. there is no ambiguity in data concatenation, and
  3. the signing hash is generated using cryptographic hashing functions.
1 Like

附录: CoBuild Hashes

本节详细介绍 Cobuild 流程中用到的各种 hash 算法。

Signing Message Hash

下面用 signing message hash 来表示 lock script 校验签名时,签名对应的数据。

Lock 的实现需要考虑钱包运行环境的影响。由于 CKB 具有模仿其他链交易签名算法的能力,CKB 用户可能使用其他链的钱包来操纵 CKB 上的资产。在这种场景中,lock 就需要考虑非 CKB 专门钱包的签名流程中 signing message 是如何计算的。由于我们无法调整这些非 CKB 专门钱包的 signing message 计算流程,而这些钱包的signing message 计算流程通常又不一样,因此 CKB lock script 实际计算 signing mesage hash 时,也不可能有统一的算法和流程。因此 CoBuild 协议只定义 signing message hash 中需要覆盖的 CKB 交易中的相关数据。CoBuild-compatible lock script 只需要确保这些必须包含的数据都被 signing message hash 覆盖即可保证安全性。

计算 signing message hash 时,首先根据当前流程执行是处于 SighashAll 还是 Otx 模式,从相应的位置取出 Message 。在所有情况下, Message 都是 lock script 一定要包含在 signing message hash 中的内容。Lock script 还需要获取与当前交易相关的一些其他数据,比如当前 tx hash,某些 cell 的内容等。然后 lock script 需要根据所处模式的不同,将 CoBuild 协议这里所规定的内容,与其他 lock script 自身逻辑需要的信息拼接起来,经过一次或多次 cryptographic hashing,即可构造出 CoBuild-compatible lock 验签使用的 signing message hash

虽然 CoBuild 流程对于 signing message hash 需要覆盖的 Message ,tx hash,cell data 这些内容没有强制的顺序要求,但是在构造 cryptographic hash 时,需要尽可能做到不同的交易会向 hash function 提供不同的源数据。

举个例子:假设一个 CoBuild-compatible lock 的 signing message hash 覆盖了 Message 和某个 cell data(注意实际场景中需要被 signing message hash 覆盖的数据不只这些,这里只是一个简单的例子),lock 的具体做法是把 Message 和 cell data 直接字符串拼接,然后进行一次 hash,得到 signing message hash。那么对于如下两种情况,这个方法生成的 signing message hash 是完全一样的:

  • Message0xaabbccddaabbccdd,而 cell data 为 0x10101010
  • Message0xaabbccdd ,而 cell data 为 0xaabbccdd10101010

这种算法显然不够好,因为对于两个不同的交易,两个不同 Message ,最后构造出的 signing message hash 是一样的,这就为伪造交易的提供了可能性。

为了避免这种问题,通常的一个做法,是 lock script 可以把一段数据的长度,拼接在这段数据的最前面。比如上面的例子,cell data 的长度分别为 4 或者 8。如果我们把 cell 的长度也作为信息 hash 到 signing message hash 中,这两种情况最终就会生成不同的 signing message hash 了。

同时注意:CoBuild 流程中会用到很多 molecule 格式的数据结构,而 molecule 具有 canonical encoding 的性质。简单来说,以 molecule 格式表述的数据结构,要么只有固定的长度,要么像 Message 这种,它的前四个字节已经包含了整个数据结构的长度。所以对于以 molecule 格式表达的数据结构,无需手动在数据前面拼上长度也能避免上述的问题。

SighashAll Variant

SighashAll 模式下, signing message hash 应覆盖如下内容:

  • 当前交易中,唯一存在的 SighashAll 结构中包含的 Message 结构。由于 Message 已经是一个 molecule 格式的数据结构 ,我们在这里拼接数据计算 signing message hash 时,并不需要把 Message 部分的长度再包含在 hash 中。
  • 可以通过 CKB syscall 获取的 tx hash,注意 tx hash 永远是 32 字节,也不需要再额外拼接 tx hash 的长度。
  • 可以通过 CKB_syscall 获取的所有 input celldata, 由于 tx hash 已经覆盖了 inputs 数量, 在这里拼接的时候不需要把再加入其中。input cell 本身以 molecule 格式的 CellOutput 结构表述,无需再拼接长度,但是 data 本身是一段可变长度的无格式二进制数据,为确保安全需要在前面拼接 data 数据的长度。
  • 超出 input cell 个数的所有 witness 的长度及内容。

计算 signing message hash 时,可以按照确定的顺序将这些内容全部拼接到一起,需要的话可以再加上 lock script specific 的数据,然后经过一次 cryptographic hash 计算得到结果。

注意这个计算方法只是一个例子,实际上 CoBuild 只要求 lock script 确保这些数据都被 cryptographic hash 覆盖即可。具体的计算方式,只要是确定定义好的,均可实现 cobuild 模式下的安全性。

再举个例子,对另一些(也许有特殊需求的)lock script,可以用如下算法:

  • 首先把 tx hash,所有的 input cell 和 data,超出 input cell 个数的所有 witness 长度及内容拼接在一起,单独为这部分数据计算一次 cryptographic hash。假设我们这里把计算出的 hash 称之为 skeleton hash
  • 接下来,lock script 把一段固定的 prefix, Message 结构以及 skeleton hash 拼接到一起,再计算一次 cryptographic hash,得到的结果,即作为 signing message hash

以下图为例:

一个 CoBuild-compatible lock script 验签时,应该在 signing message hash 中包含如下内容:

  • 当前交易中 SighashAll 类型的 witness 中包含的 Message 结构
  • 当前的 tx hash
  • I1 对应的 CellOutput, 以 molecule 格式 序列化
  • I1 对应的 CellOutput Data, 以 little-endian encoding 的 u32 表示的 Data 长度和 Data 的实际内容
  • I2 对应的 CellOutput, 以 molecule 格式 序列化
  • I2 对应的 CellOutput Data, 以 little-endian encoding 的 u32 表示的 Data 长度和 Data 的实际内容
  • 以 little-endian encoding 的 u32 表示的 Witness 3 的长度和 Witness 3 的实际内容
  • 以 little-endian encoding 的 u32 表示的 Witness 4 的长度和 Witness 4 的实际内容
  • 以 little-endian encoding 的 u32 表示的 Witness 5 的长度和 Witness 5 的实际内容

SighashAllOnlyVariant

WitnessLayout 除了 SighashAll Variant, 还有一个比较特殊的 SighashAllOnly Variant,这个 Variant 中只有 seal 没有 message。 如果在一个交易中,所有的 witness 都是 SighashAllOnly 类型,那意味着这个交易没有包含任何 Message

在这种情况下,lock script 验签时,应该在 signing message hash 中覆盖如下内容:

  • 可以通过 CKB syscall 获取的 tx hash
  • 可以通过 CKB_syscall 获取的所有 input celldata, 由于 tx hash 已经覆盖了 inputs 数量, 在这里拼接的时候不需要把再加入其中
  • 超出 input cell 个数的所有 witness 的长度及内容

注意如果一个 lock script 要同时支持包含和不包含 Message 的模式,应该通过添加不同的 prefix 或者利用哈希算法自带的方法(比如 blake2b 算法提供的 personalization),区分开包含 Message 以及不包含 Message 的两类 transaction,以避免这两类不同的 transaction,误生成相同的 signing message hash

Otx Variant

Otx 模式下, signing message hash 应覆盖如下内容:

  • 对应当前 Otx 的 witness 中包含的 Message 结构
  • Otx 覆盖的 input cells 的个数,以 CellInput 格式表示的所有 input cells 的内容, 以 CellOutput 格式表达原始数据, 以及以 raw data 形式存在的原始 Cell Data
  • Otx 覆盖的 output cells 的个数,以及所有 output cells 的内容,包含以 CellOutput 格式表达的结构,和 raw data 形式存在的 cell data
  • Otx 覆盖的 cell deps 的个数,以及以 CellDep 格式表示的所有 cell deps 的内容
  • Otx 覆盖的 header deps 的个数,以及以 Byte32 格式表示的所有 header deps 的内容

注意这里以 raw data 形式存在的 cell data 是变长的无格式字符串,为确保安全,需要在这些 cell data 的前面相应拼接上 cell data 字段的长度。除此之外,这里包含的其他数据结构均为 molecule 类型不需要再拼接长度。

计算 signing message hash 时,可以按照确定的顺序将这些内容全部拼接到一起,需要的话可以再加上 lock script specific 的数据,然后经过一次 cryptographic hash 计算得到结果。

出于安全性的考虑,我们建议区分开 SighashAll 模式与 Otx 模式的交易,在计算 signing message hash 时使用不同的 hashing prefix 或是 hash function 自带的方法(如 blake2b 中用不同的 personalization)。

更具体的说,CoBuild 规范建议为如下三个不同场景,都使用不同的 prefix:

  • SighashAll 模式,带有 Message 的交易
  • SighashAllOnly 模式,没有 Message 的交易
  • Otx 模式的交易

以下图为例:

一个 CoBuild-compatible lock script 验签时,应该在 signing message hash 中覆盖如下内容:

  • 当前 otx 中 唯一的,以 Otx 类型存在的 witness 中包含的 Message 结构
  • 2 (表示 input cells 的个数,以 little endian encoding 的 u32 表示)
  • I1 对应的 CellInput 结构(注意以 molecule 格式表述的 CellInput 结构已经包含长度信息,无需单独再包含 CellInput 结构的长度,下文中对所有 molecule 相关的结构,均类似处理)
  • I1 对应的 CellOutput, 以 molecule 格式 序列化
  • I1 对应的 CellOutput Data, 以 little-endian encoding 的 u32 表示的 Data 长度和 Data 的实际内容
  • I2 对应的 CellInput 结构
  • I2 对应的 CellOutput, 以 molecule 格式 序列化
  • I2 对应的 CellOutput Data, 以 little-endian encoding 的 u32 表示的 Data 长度和 Data 的实际内容
  • 3 (表示 output cells 的个数,以 little endian encoding 的 u32 表示)
  • O1 对应的 CellOutput 结构
  • O1 对应的 cell data 长度
  • O1 对应的 cell data 内容
  • O2 对应的 CellOutput 结构
  • O2 对应的 cell data 长度
  • O2 对应的 cell data 内容
  • O3 对应的 CellOutput 结构
  • O3 对应的 cell data 长度
  • O3 对应的 cell data 内容
  • 1 (表示 cell deps 的个数,以 little endian encoding 的 u32 表示)
  • D1 对应的 CellDep 结构
  • 4 (表示 header deps 的个数,以 little endian encoding 的 u32 表示)
  • H1 对应的 Byte32 结构
  • H2 对应的 Byte32 结构
  • H3 对应的 Byte32 结构
  • H4 对应的 Byte32 结构

Example

这个仓库 提供了计算 signing message hash 的样例实现。

这个样例展示了在三种不同的模式中,根据 CoBuild 规范要求把需要覆盖的数据拼接在一起,经过一次 blake2b hash 计算得到结果,作为 signing message hash

在样例的三种不同模式中,我们选用了不同的 blake2b personalization,来避免对不同类型的交易误生成同样的 hash:

  • SighashAll 模式,带有 Message 的交易: 使用 ckb-tcob-sighash
  • SighashAllOnly 模式,没有 Message 的交易: 使用 ckb-tcob-sgohash (‘sgo’ means ‘sighash only’)
  • Otx 模式: 使用 ckb-tcob-otxhash

如上文所说,这里只是一种 signing message hash 的实现样例。对一个 CoBuild-compatible lock script 来说,只要在计算 signing message hash 的过程中正确的覆盖了 CoBuild 规范中指定的所有数据,并且数据拼接不会产生歧义,最终对通过 cryptographic hash function 对拼接好的数据生成一个 hash 进行验签,即可保证协议上的安全性。

Appendix: Lock/Type Script Validation Rules

中文版本

This section describes the validation rules that a lock/type script needs to include in order to meet the requirements of the CoBuild protocol.

From a convenience and practicality standpoint, we recommend all lock/type scripts support legacy WitnessArgs, then add support for SighashAll Variant, and support for Otx Variant.

Legacy Flow Handling

Considering forward compatibility, lock / type scripts that support the CoBuild protocol may also need to support the legacy flow, which is the existing process based on WitnessArgs structure.

A lock/type script can simultaneously support both CoBuild flow and legacy flow. However, for a specific transaction, a lock/type script must choose one of the two for verification: either use CoBuild flow or use legacy flow.

Usually, all scripts in a transaction should use the same flow for verification. But there’s an exception: if a tranasction uses script A that supports both flows and script B that only supports legacy flow, it’s possible that script A verifies according to CoBuild flow while script B verifies according to legacy flow.

Lock/type scripts should follow these rules to determine the flow to use:

  • If there is a witness of the type WitnessLayout in current transaction, use the CoBuild Flow.
    • Note: The condition here is its existence, not uniqueness, not if different WitnessLayout witnesses coexist, nor its compliance with other CoBuild rules.
  • Otherwise, use the Legacy Flow.

Lock Script

The main responsibility of a CKB lock script is to perform ownership (owner signature) verification - checking that the input cell is unlocked under the correct conditions, while also ensuring that data in transactions are not tampered with. A lock script that supports both SighashAll and Otx modes should verify transactions according to the following rules:

  1. Maintain a set of variables: is, ie, os, oe, cs, ce, hs, he and set them all to 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
  2. Loop through all witnesses of current tx (loop variable i), check if there’s a witness in current tx using WitnessLayout::OtxStart type.
  3. If no witness in current tx uses WitnessLayout::OtxStart, skip to step 8 for further validation.
  4. Ensure only one witness is of type WitnessLayout::OtxStart in current tx or else exit with an error message. Set loop variable i as this witness’s index value.
  5. Set values of is and ie as start_input_cell in OtxStart, os and oe as start_output_cell in OtxStart, cs and ce as start_cell_deps in OtxStart, hs and he as start_header_deps in OtxStart.
  6. Starting from witness at index i+1 , loop through each WitnessLayout::Otx typed witness (exit loop directly if encounter any witness not in WitnessLayout::Otx type), validate each WitnessLayout::Otx witness (i.e. each Otx in this tx) as follows:
    • a. Let variable ‘otx’ store the WitnessLayout::Otx type witness, then the following data constitutes current Otx (open transaction):
      • i. Input cells with index within [ie, ie + otx.input_cells)
      • ii. Output cells with index within [oe, oe + otx.output_cells)
      • iii. Cell deps with index within [ce, ce + otx.cell_deps)
      • iv. Header deps with index within [he, he + otx.header_deps)
      • v. The current WitnessLayout::Otx typed witness
    • b. Check otx, if all values for ‘otx.input_cells’, ‘otx.output_cells’, ‘otx.cell_deps’, and ‘otx.header_deps’ are 0, the script should exit with an error.
    • c. Parse otx.message, check each Action in the message, the script_hash in Action must either be the same as the type script hash of one input/output cells in transaction, or be the same as the lock script hash of one input cells in transaction. Otherwise, the script shall terminate with an error.
      • Note that this check applies to the entire transaction not just current otx.
    • d. Check input cells in the range [ie, ie + ox.input_cells), skip to next iteration if none use current lock script.
    • e. Calculate corresponding signing message hash based on Otx mode rules defined in Appendix: CoBuild Hashes.
    • f. Fetch SealPair from seals, using the current lock script’s lock script hash as key . Exit with error if not found.
    • g. Extract signature from SealPair according to the lock’s own logic, verify it against previously generated signing message hash. Exit with error if verification fails.
    • h. ie += otx.input_cells; oe += otx.output_cells; ce += otx.cell_deps; he += otx.header_deps
  7. Assume the first witness not of type WitnessLayout::Otx starting from index i+1 is at index j , check that all witnesses in the the range [0, i) and [j, +infinity) are not of type WitnessLayout::Otx or else exit with an error.
  8. Check the input cells within the range of [0, is) and [ie, +infinity), if any of them uses current lock script, perform a SighashAll signature verification:
    • a. Check if there’s a WitnessLayout::SighashAll variant among all witnesses in the current tx.
    • b. If there exists a witness of type WitnessLayout::SighashAll, ensure that only one such witness exists in the current tx and store its witness.message in variable message. If no such witness exists in the current tx, set variable message as empty.
    • c. Assuming that message is not empty, check that for each Action contained in message, the script_hash in Action must either be the same as the type script hash of one input/output cells in transaction, or be the same as the lock script hash of one input cells in transaction. Otherwise, the script shall terminate with an error.
    • d. Read the first witness from the script group corresponding to current lock script. If this witness isn’t of type WitnessLayout::SighashAll or WitnessLayout::SighashAllOnly, exit with error.
    • e. Ensure that apart from the first witness position inside current lock script group, there’re either no other witnesses or they’re empty.
    • f. Extract seal from the first witness within current lock script group and retrieve signature according to the lock script’s own custom logic.
    • g. Calculate signing message hash by following the SighashAll/SighashAllOnly rules outlined under Appendix: CoBuild Hashes.
    • h. Verify the extracted signature along with the signing message hash; if verification fails then exit with error.

Type Script

CKB type scripts mainly verify application-specific on-chain state transitions. For instance, in UDT type scripts validate token issuance and transfer. In CoBuild process, apart from validating those state transitions, type scripts should also verify consistency between message and full/open transaction behaviors. During message consistency verification, a type script needs to validate each otx individually as well as a possible SighashAll witness containing message.

A type script supporting both SighashAll and Otx variant should follow these validation rules:

  1. Execute the app-specific state transition logic defined by the type script.
  2. Maintain a set of variables is, ie, os, oe, cs, ce, hs, he all initialized to 0.
  3. Loop through all witnesses in current tx (loop variable i), checking if there’s a witness of type WitnessLayout::OtxStart.
  4. If no witness of type WitnessLayout::OtxStart exists in current tx, jump to step 9 for further verification.
  5. Ensure only one witness is of type WitnessLayout::OtxStart in the transaction; otherwise exit with error. Set variable i as index value of this witness.
  6. Set is and ie values equal to start_input_cell in OtxStart; similarly set os and oe values equal to start_output_cell in OtxStart; set cs and ce values equal to start_cell_deps in OtxStart; finally, set hs and he values equal to start_header_deps in OtxStart.
  7. Starting from the witness with index of i + 1, loop through each witness of type WitnessLayout::Otx (exit the loop immediately when encountering a witness that is not of type WitnessLayout::Otx) and perform the following verification for each WitnessLayout::Otx typed witness (i.e. for each Otx in tx):
    • a. Assume that the variable otx stores the witness of type WitnessLayout::Otx, then the following data constitutes the current Otx (Open Transaction):
      • i. Input cells within index range [ie, ie + otx.input_cells)
      • ii. Output cells within index range [oe, oe + otx.output_cells)
      • iii. Cell deps within index range [ce, ce + otx.cell_deps)
      • iv. Header deps within index range [he, he + otx.header_deps)
      • v. The currently processed witness of type WitnessLayout::Otx
    • b. Check input cells within the range of [ie, ie+otx.input_cells) and output cells within the range of [oe, oe+otx.output_cells). If all these input/output cells do not use current type script then skip to next iteration.
    • c. Verify message in WitnessLayout::OTX based on current OTX scope. (Refer to the next section for specific verification process).
    • d.ie += otx.input_cells; oe += otc.output_cells; ce += otz.cell_deps; he+= otz.header_deps
  8. Assume starting from the witness at index i+1, the first witness that isn’t of type WitnessLayout::OTX has an index j. Make sure witnesses within [0,i) and [j,+infinity) ranges are not of Witnesslayout::OTX type, otherwise exit with error.
  9. Check input and output cells within [0, is) and [ie, +infinity), if any of these cells use the current type script, perform the following verification:
    • a. Scan all witnesses of the current transaction to determine whether there is a witness of WitnessLayout::SighashAll type.
    • b. If no witness of WitnessLayout::SighashAll type exists in the current transaction, consider the script validation successful and exit execution with return code 0.
    • c. Check if there’s exactly one Witnesslayout::SighashAll typed witness in current tx, else exit with an error.
    • d. Verify message in WitnessLayout::SighashAll, based on the scope including input cells within [0, is) & [ie, +infinity) and output cells within [0, os) & [oe, +infinity). (Refer to the next section for specific verification rules.)

Message Validation

This section describes how the type script validates messages.

Whether it’s an Otx or SighashAll mode transaction, we need to validate the Action in message corresponding with this type script against a range of related input/output cells.

Note that the validation is against “a range of cells”: due to the possible existence of otx , this range may not necessarily be the entire tx but could be intervals like [3,6), or the union of two intervals such as [0,4) || [8,11). However there will only ever be union of two ranges maximum; never more than that.

The type script needs to traverse the actions in message, find an Action where Action.script_hash matches its own script hash. This Action is the one corresponding to the current type script.

  • If multiple Actions are found that correspond to the type script hash, it is recommended to throw an error. The purpose is to prevent third parties from inserting Actions calling the same type script into the transaction later to override user’s Action.
    • Third-party Actions may appear after user’s Action on wallet UX or even be hide due to insufficient display space, leading users to believe that only their own actions are being signed.
  • Example code

Note: The on-chain type script cannot fetch the complete ScriptInfo of the current Action, nor can it verify the correctness of Action.script_info_hash.

Therefore, type scripts can make the following two assumptions:

  • The content of Action, including Action.script_info_hash, has been covered by lock scripts during verification process, ensuring no tampering.
  • In the signing process, wallets will get the correct ScriptInfo through some offchain channel, verify the consistency between the ScriptInfo and Action.script_info_hash, parse ‘Action.data’ with the correct ‘ScriptInfo.schema’, and present the correctly parsed data to the cell holder for signing.

Although a type script does not have the ScriptInfo, it should have information like ScriptInfo.schema and ScriptInfo.message_type in its source code. Type scripts can use these information to parse Action.data, get the structured action data (as input) and validating cells. For instance, if Action.data presents a Spore NFT Mint operation, the type script needs to verify that cells within certain scope indeed expressed a mint operation; if Action.data presents a UDT transfer, similar verification is required by type script to ensure correct amount of UDT has been transferred to the right recipient and change UDT returned to payer.

1 Like

附录: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 规则。
  • 否则,使用 legacy flow 进行校验。

Lock Script

CKB lock script 的主要任务是进行所有权(所有者签名)验证——校验 input cell 以正确的条件被解锁,同时也确保交易中的数据不被篡改。一个同时支持 SighashAll 以及 Otx 模式的 lock script,应按照如下规则对交易进行校验:

  1. 维护一组变量 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
  2. 循环扫描当前 tx 的所有 witnesses (循环变量 i),检查当前 tx 中,是否有一个 witness,使用 WitnessLayout::OtxStart 类型
  3. 如果当前 tx 中不存在 WitnessLayout::OtxStart 类型的 witness,跳到 step 8 继续执行验证
  4. 确保当前 tx 中只有一个 witness 使用 WitnessLayout::OtxStart 结构,否则报错退出。并把当前循环变量 i 设置为该 witness 的 index 值。
  5. 把 is 和 ie 置为 OtxStart 中的 start_input_cell 值,把 os 和 oe 置为 OtxStart 中的 start_output_cell 值,把 cs 和 ce 置为 OtxStart 中的 start_cell_deps 值,把 hs 和 he 置为 OtxStart 中的 start_header_deps
  6. 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
    • 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 与 Actionscript_hash 相同,要么存在一个 input cell,它的 lock script hash 与 Actionscript_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
  7. 假设从 indexi + 1 的 witness 开始,第一个不为 WitnessLayout::Otx 类型的 witness,indexj。检查当前 tx 中,index 在 [0, i) 以及 [j, +infinity) 范围内的 witness 都不是 WitnessLayout::Otx 类型,否则报错退出。
  8. 检查 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 与 Actionscript_hash 相同,要么存在一个 input cell,它的 lock script hash 与 Actionscript_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 进行验签。如果验签失败,则报错退出。

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 进行校验:

  1. 执行 type script 自定义状态迁移逻辑的校验。
  2. 维护一组变量 is, ie, os, oe, cs, ce, hs, he, 并全部置为 0。
  3. 循环扫描当前 tx 的所有 witnesses (循环变量 i),检查当前 tx 中,是否有一个 witness,使用 WitnessLayout::OtxStart 类型。
  4. 如果当前 tx 中不存在 WitnessLayout::OtxStart 类型的 witness,跳到 step 9 继续执行验证。
  5. 确保当前 tx 中只有一个 witness 使用 WitnessLayout::OtxStart 结构,否则报错退出。把变量 i 设置为该 witness 的 index 值。
  6. 把 is 和 ie 置为 OtxStart 中的 start_input_cell 值,把 os 和 oe 置为 OtxStart 中的 start_output_cell 值,把 cs 和 ce 置为 OtxStart 中的 start_cell_deps 值,把 hs 和 he 置为 OtxStart 中的 start_header_deps
  7. 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
    • 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
  8. 假设从 indexi + 1 的 witness 开始,第一个不为 WitnessLayout::Otx 类型的 witness,index 为 j。检查当前 tx 中,index[0, i) 以及 [j, +infinity) 范围内的 witness 都不使用 WitnessLayout::Otx 类型,否则报错退出
  9. 检查 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 进行校验(具体校验过程参考下一小节)

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 在验签过程中覆盖以确保无法篡改。
  • 在生成签名的过程中,钱包应该使用正确的 ScriptInfoAction.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 返还给了支付方。

Appendix: Custom Union Ids in WitnessLayout

中文版本

In the WitnessLayout structure, each union variant has a special custom union id (a larger magic number). This is a trick used to maintain forward compatibility: such special values allow WitnessLayout and previous WitnessArgs to coexist without affecting each other.

The widely used WitnessArgs is a molecule table containing three fields. Its serialized data looks roughly like this:

<length> <offset 1> <offset 2> <offset 3> ...

Both length and offset are of little endian u32 type.

And the serialization of a molecule union looks something like this:

<union id> ....

Here, the union id is also of little endian u32 type.

For WitnessLayout, it currently uses custom union ids such as 0xFF000001, 0xFF000002. Considering future expansion, WitnessLayout should only use the range from 0xFF000001 to 0xFFFFFFFF for new variants’ union ids (this range has over 16 million values which are enough).

Why? Assuming there’s some serialized data in the format of WitnessLayout, if you read out its first four bytes as little endian u32, the result will definitely not be less than 0xFF000001. If you try to parse this data using the WitnessArgs type, these first four bytes would be read out as length for whole data. For example, 0xFF000001 == 4278190081 ~=4080MB. But CKB block size maxes at around 600k, even considering future expansion, it’s hard to reach 4000MB. Therefore, it is practically impossible to construct a WitnessArgs of length 0xFF000001 or longer. So if you parse the binary data serialized in the format of WitnessLayout as WitnessArgs, the molecule verification will definitely fail.

Similarly, for data serialized in the format of WitnessArgs, the value read from its first four bytes would be far less than 0xFF000001, and could not possibly be any union id used by WitnessLayout. Thus if you try parsing this data as WitnessLayout, molecule verification will also certainly fail.

Therefore, in transaction witnesses you can arbitrarily insert either WitnessArgs formatted or WitnessLayout formatted data. On-chain scripts can deterministically determine what format a certain witness uses. Data in WitnessLayout format will surely fail validation when parsed as WitnessArgs, and vice versa. In future we can further expand WitnessLayout to support more variants.

If we look at the encoding, the WitnessLayout union places a 4-byte ID in front of the actual witness content. This 4-byte ID works like a version flag, helps differentiate new format from existing WitnessArgs.

1 Like

附录:Custom Union Ids in WitnessLayout

WitnessLayout 结构中,每一个 union variant 都有一个特殊的 custom union id(一个较大的magic number)。这是一个为了保持向前兼容用的技巧:通过这样特殊的取值可以让 WitnessLayout 和之前的 WitnessArgs 互不影响实现共存.

现在广泛使用的 WitnessArgs 是一个包含三个字段的 molecule table,它序列化后的数据大致是这样子的:

<length> <offset 1> <offset 2> <offset 3> ...

其中 lengthoffset 都是 little endian 的 u32 类型。

而一个 molecule union 的序列化结构大致是如下的样子:

<union id> ....

这里的 union id 也是 little endian 的 u32 类型。

对于 WitnessLayout 来说,它目前使用的 union id 是 0xFF000001, 0xFF000002 …。考虑未来扩充,WitnessLayout 应该只使用 0xFF000001 - 0xFFFFFFFF 这个范围作为这个类型允许的 union id 范围(这个范围有超过 16 million 个值,足够使用)。

假设有一个 WitnessLayout 格式序列化后的数据,这段数据前四个字节如果作为 little endian 的 u32 读取出来肯定不小于 0xFF000001。如果尝试把这段数据以 WitnessArgs 的类型进行解析,这段数据的前四个字节会作为整段数据的长度读取出来,e.g. 0xFF000001 == 4278190081 ~= 4080MB。但是 CKB 一个 block 最大只有 600k 左右,即使考虑将来可能的扩充,也很难达到 4000MB。因此实际上不可能构造出一个长度为 0xFF000001 甚至更长的 WitnessArgs 结构。所以如果把 WitnessLayout 格式序列化后的二进制数据,以 WitnessArgs 格式来解析,molecule 验证过程一定失败。

同样的,对于以 WitnessArgs 格式序列化好的数据,前四个字节读出来的值一定远远小于 0xFF000001,不可能是 WitnessLayout 使用的 union id。所以以 WitnessArgs 格式序列化好的数据,如果以 WitnessLayout 的结构来解析,molecule 验证过程也一定会失败。

因此交易 witness 中可以任意塞入 WitnessArgs 格式或是 WitnessLayout 格式的数据。链上合约也可以确定性的判断某一个 witness 使用的是什么格式。WitnessLayout 格式的数据以 WitnessArgs 格式解析校验一定会失败,同样的,WitnessArgs 格式的数据以 WitnessLayout 格式解析校验也一定会失败。未来还可以进一步扩充 WitnessLayout 格式,支持更多的可选类型。

如果只看编码的话,这里的 WitnessLayout union,也很是像先在 witness 上放置一个 4 个 bytes 的 ID,然后再放置实际的 witness 内容。同时靠这个 ID 与现有的 WitnessArgs 结构可以区分开来。WitnessLayout 通过 custome union id 实现了类似 version flag 的功能。

Appendix: CoBuild-Only or CoBuild+Legacy?

中文版本

There are currently two input data (witness) encoding modes for CKB scripts:

  • CoBuild mode: Encode input data in the WitnessLayout format defined in this document;
  • Legacy mode: Encode input data in the WitnessArgs format.

CKB transactions only care whether the included scripts and witnesses can be verified, not how these scripts parse witnesses. This means that scripts using different witness encoding mode can coexist within a transaction.

A CKB script can also support both modes: for example, such a script can first look for witness in the WitnessLayout format, and if it’s missing, it tries to find a WitnessArgs witness.

Usually when ‘CoBuild mode’ or ‘Legacy mode’ is mentioned, it refers to a certain script being in one of these modes, after deciding which input data encoding to use. At this point, this script has determined either to only read from WitnessLayout witnesses (CoBuild Mode), or only from WitnessArgs witnesses (Legacy Mode).

However when discussing “a script supports CoBuild mode” below , it means that a script is able to operate under CoBuild mode (not “only” or “already determined”) and read from WitnessLayout witnesses. Correspondingly, “a script supports Legacy mode”, means that this script is able to operate under Legacy Mode (not “only” or “already determined”) and reads from WitnessArgs witnesses.

Lock Script

In the transition period (from now until CoBuild becomes widely used as default), we do not recommend lock script supporting CoBuild mode only without supporting Legacy mode.

It’s not difficult for a lock script to support both CoBuild mode and Legacy mode. The two modes’ difference lie only on the calculation of signing message hash. A well structured lock script could reuse almost all verification codes in the two modes.

If a lock script wants to support as many CKB apps as possible, the optimal solution is to support both CoBuild and Legacy modes.

Type Script

The considerations for type script are somewhat different from those of lock script.

Without considering some extreme cases in OTX, a lock script that only supports CoBuild mode cannot be used with a type script that can only process WitnessArgs typed witnesses in the same cell.

What about the other way around? Can a type script that only supports CoBuild mode be used with a lock script (such as the default secp256k1-blake160 lock) that can only process WitnessArgs typed witnesses? The answer is yes.

Let’s look at this example:

Assume here I1 and I2 both use the secp256k1-blake160 lock, while I1 uses a type script that only supports CoBuild mode. A valid transaction can use the above layout: witness #0 and witness #1 are of the WitnessArgs type, witness #2 is of CoBuild mode’s WitnessLayout::SighashAll type which contains an Action required by the type script in I1.

According to the conventions used in legacy lock scripts, although the lock scripts used by I1 and I2 cannot parse the WitnessLayout typed witness#2, these lock scripts will still sign all data of witness#2. Meanwhile, the type script used by I1 could parse Message from witness#2, and find its corresponding Action to complete the verification.

We can see that there won’t be compatibility issues for type scripts supporting only CoBuild mode without supporting Legacy mode. For type script, you could choose either to support CoBuild mode or be compatible with both modes. Our suggestion new type scripts to consider supporting CoBuild mode first, and being compatible with Legacy mode if necessary.

Action May Not Exist

In most cases, there will be a WitnessLayout::SighashAll witness in CoBuild transactions, which contains the Message, as well as the Actions corresponding to the type scripts used. However, it should be noted that the CoBuild protocol does not require that a WitnessLayout::SighashAll must exist.

Suppose there is a type script that only supports CoBuild mode. One problem it needs to face is: Should it be able to handle situations where its own Action is missing when a WitnessLayout::SighashAll doesn’t exist?

Different type scripts can take different actions here based on their own requirements. A type script can choose whether to exit with an error directly or ignore the issue and continue verification when its required ‘Action’ does not exist.

It should be pointed out that for most scenarios, existence of an Action can greatly simplify on-chain verification. It’s usually difficult to guess the user action by reverse analyzing transaction data.

Therefore, we suggest adding this validation rule for type scripts: it must find the required Action in transaction before it can proceed.

NervosDAO Type Script

The NervosDAO type script is a special example for CoBuild process because its code was deployed into genesis block with unspendable lock (lock hash = 0x0000…, all zero). This means NervosDAO cannot be upgraded via methods other than hard fork.

If we still want the NervosDAO type script to be compatible with CoBuild without upgrading it through hard fork, additional special handling is needed. One approach is to hardcode specific logic related to NervosDAO within CoBuild toolchain. If a CoBuild tool detects a transaction involving NervosDAO, it should perform some preprocessing:

  • If the transaction does not contain a NervosDAO Action, CoBuild tools should construct one using raw transaction data for UX presentation.
  • If it contains a NervosDAO Action, these tools should validate the action, take the responsibility that would normally be taken by the NervosDAO type script.

Remember, during transition period (from now until CoBuild becomes widely used), we don’t recommend CKB lock scripts to only support CoBuild mode without supporting Legacy mode. However, due to the speciality of Nervos DAO type script, we suggest that for all operations involving Nervos DAO, the tools should generate transactions using WitnessArgs regardless of whether or not the lock scripts used support CoBuild. Even if for some operations the generated transaction can use CoBuild because the NervosDAO type script doesn’t need to read witness in that case, the generated transaction shouldn’t. This way we simplify implementation by the requirement of using WitnessArgs in all cases.

Then fully support for CoBuild can be added to NervosDAO type script when it is going to be upgraded for some reason in future (no plan for now).

1 Like

附录:CoBuild-Only or CoBuild+Legacy?

CKB script 现在有两种输入数据 (witness) 编码模式:

  • Cobuild 模式:使用本文定义的 WitnessLayout 格式读取数据
  • Legacy 模式:使用 WitnessArgs 格式读取数据

CKB 交易只关心包含的 scripts 和 witnesses 是否能验证通过,并不关心这些 scripts 是如何解析 witnesses 的。这意味着一个交易里面使用不同输入数据编码的 scripts 可以共存。

一个 CKB script 也可以做到两种模式都支持:例如优先寻找 WitnessLayout 格式读取数据,发现WitnessLayout 类型的 witness 缺失时,再从 WitnessArgs 格式中读取数据。

通常我们讨论 CoBuild 模式或者 Legacy 模式时,是指某一个 script 在已经确定了要用哪种输入数据格式后所处的某一个模式。此时这个 script 已经确定:要么只从 WitnessLayout 中读取数据(CoBuild 模式),要么只从 WitnessArgs 中读取数据(Legacy 模式)。

但是在下文中谈到 “一个 script 支持 CoBuild 模式”,是指一个 script 可以运作在 CoBuild 模式下(而不是“只能”或者“已经确定”运作在 CoBuild 模式下),从 WitnessLayout 中读取数据。与此对应的,“一个 script 支持 legacy 模式”,是指这个合约可以运作在 Legacy 模式下(而不是“只能”或者“已经确定”运行在 Legacy 模式下),从 WitnessArgs 中读取数据。

Lock Script

在过渡时期(现在直到 CoBuild 被广泛使用成为默认之前),我们不建议一个 lock script 只支持 CoBuild 模式而不支持 Legacy 模式。

Lock script 要同时支持 CoBuild 模式与 Legacy 模式并不复杂。它们的差异点只在 signing message hash 的局部计算上。除此之外,一个架构良好的 lock script 完全可以在两种模式下共享其余几乎所有的验签代码。

如果一个 lock script / 钱包希望尽可能支持尽可能多的 CKB apps,那么同时支持 CoBuild 模式以及 Legacy 模式是最优解。

Type Script

Type script 的考量点与 lock script 有一些不同。

不考虑 OTX 中一些极端情况,一个只支持 CoBuild 模式的 lock script,是无法与一个只能从 WitnessArgs 类型的 witness 中读取输入数据的 type script 在一个 cell 中一起使用的。

如果反过来呢?一个只支持 CoBuild 模式的 type script,可不可以与一个只能从 WitnessArgs 类型的 witness 中读取签名的 lock script(比如创始块中的单签多签)一起使用?答案是可以。

让我么看下面这个例子:

假设这里 I1 和 I2 都使用了创始块中的单签 lock,同时 I1 中使用了一个只支持 CoBuild 模式的 type script。一个有效的交易可以使用如上布局:witness #0 和 witness #1 使用 WitnessArgs 类型,witness #2 使用 CoBuild 模式的 WitnessLayout::SighashAll 类型,包含 I1 中 type script 所需要的 Action

按照传统意义上 lock script 的 convention,虽然 I1 和 I2 使用的 lock script 不能解析 witness #2 所使用的 WitnessLayout 格式,但是这里的 lock script 依然会把 witness #2 的全部数据签入签名。同时,I1 所使用的 type script 可以从 witness #2 中解析 Message,并找到对应自己的 Action。完成校验。

由此可以看出,只支持 CoBuild 模式不支持 Legacy 模式的 type script 不会有兼容性问题。对于 type script 来说,可以自行选择是只支持 CoBuild 模式,还是两种模式都兼容。我们的建议是实现 type script 时默认只支持 CoBuild 模式,有特别需要再考虑兼容 Legacy 模式。

Action 可能不存在

大部分情况下,CoBuild 交易中都会有 WitnessLayout::SighashAll witness,其中包含 Message,也包含当前交易用到的 type script 所对应的 Actions。但需要注意的是,CoBuild 协议没有要求 WitnessLayout::SighashAll 必须存在。

假设有一个 type script 只支持 CoBuild 模式,它需要面对的一个问题是:它是否应该能够处理因为 WitnessLayout::SighashAll 不存在,导致当前交易中没有对应自己的 Action 的情况呢?

对于这个问题,不同的 type script 需要根据自己的需求做决定。虽然 CoBuild 协议上没有禁止 Action 不存在的情况,但是 type script 可以选择在自己需要的 Action 不存在时,是直接报错退出,还是忽略 Action 继续校验。

应当指出的是,对于大部分场景,Action 的存在可以大大简化链上校验逻辑。不是所有的场景都能通过识别交易而(容易地)反向分析出输入数据。

因此对于 type script, 我们建议增加一条校验规则:一定要能在交易中找到所需的 Action 才能继续执行。

NervosDAO Type Script

NervosDAO type script 对于 CoBuild 流程来说是一个非常特殊的例子,它的特别之处在于其 type script 代码是以一个无法花费的 lock (lock hash = 0x0000…, all zero) 部署在创世块中的,这意味着 NervosDAO type script 无法通过硬分叉之外的方法升级。

如果想要在不做硬分叉不升级 NervosDAO type script 的情况下让它能兼容 CoBuild 流程,需要额外的特殊处理。一种做法是,在支撑 CoBuild 工具中 hardcode 针对 NervosDAO 的特殊逻辑,如果发现一个交易是 Nervos DAO 交易,就先做一些预处理:

  • 如果这个交易不包含 Nervos DAO Action,CoBuild 工具应该利用 raw tx data 构造一个可以用于前端呈现的 Nervos DAO Action
  • 如果这个交易包含 Nervos DAO Action,CoBuild 工具应该对这个 Nervos DAO Action 进行校验,在前端实现本应该是 Nervos DAO type script 要做的工作。

注意前面说过:在过渡时期(现在直到 CoBuild 被广泛使用成为默认之前),我们不建议一个 CKB lock script 只支持 CoBuild 模式而不支持 Legacy 模式。但由于 Nervos DAO type script 的特殊性,我们建议对于所有 Nervos DAO 操作,无论一个 lock 支持 CoBuild 模式与否,都应该只生成使用 WitnessArgs 类型的交易。即使有部分操作因为 Nervos DAO 本身不需要读取 witness,可以用 CoBuild 来做,也不应该用 CoBuild 来做,这样把所有情况都统一成用 WitnessArgs,简化实现。

在这个基础上,等未来某一天 Nervos DAO type script 由于某个原因升级时(目前没有明确计划),再通过升级 Nervos DAO type script,实现对 CoBuild 的完全支持。

能不能在github里备份一份,论坛里有好多这种高质量的design(草稿),不放github以后不好找

Can Action add a new option? In addition to constraining type_script through script_hash, you can also set code_hash, hash_type, and args. Then lock will be responsible for calling the corresponding script through dynamic linking or spawn, checking, and ensuring that the return value is correct. I’ve thought of some usage scenarios.

Action能不能新增一种选项,除了通过script_hash约束type_script以外,还可以设置 code_hash, hash_type, args,然后lock会负责通过动态链接或者spawn来调用对应的脚本,进行检查,同时确保返回值正确。我已经思考到了一些使用场景。

1 Like

Personally I feel it’s better to include a special Action using lock’s own script hash as script_hash, then the data of this particular Action can contain code_hash, hash_type or args as you wish.

When the very lock script captures the designated Action, it can then execute passed code_hash / hash_type as it wishes, via exec / spawn / dynamic linking or whatever solution it defines.

My concern is that it could be very hard to reach a consensus so every lock script wants to do dynamic linking / spawn / exec. It will bring a completely different set of security assumptions. So I would consider it makes more sense to introduce this first on per-lock-script basis, later if this is really becoming a popular design, cobuild can then introduce it in a V2 design.

1 Like

Using lock’s own script hash as script_hash is inconvenient, it’s make the code invoked by dynamic linking, exec or spawn hard to find which otx called it.

Compared to binding verification to a specific cell’s type script, using exec allows for a more abstract expression of the user’s intention. The invoked code then checks the entire transaction to assess whether the user’s intention has been met, without needing to specify which component fulfills this intent.

@xxuejie @janx can you confirm or deny the following statement?

CoBuild validation rules require all Action object to have matching type script in the fully assembled CKB Transaction, so a Type Lock is required for validating Lock Script Actions by CoBuild, especially in a OTX environment.

Actually we’ve relaxed this restriction(tho I was on vacation in the past couple of days so never got a chance to edit): an Action object can have either a matching type script, or a lock script. So a lock script can have Action object, there is no need to introduce a type script when not needed.

1 Like

CoBuild is a way of representing the intentions of transitions happening within a TX / OTX. But let’s say my interface get wrong the CKB change cell by 1M CKB, there is no indication that 1M CKB is up for grabs (either going to the miner or to someone who submit an OTX spending that 1M).

Then again there may be use-cases where a OTX may intentionally leave this unbalanced, for example limit orders. An OTX may have a negative delta of CKB (- 1000 CKB) and positive delta of a UDT (+990 iCKB UDT). This would be a sell order of 990 iCKB UDT for 1000 CKB.

More commonly, exists a small positive CKB delta for paying the miner/aggregator fee.

@xxuejie is there a standard way to represent in CoBuild these phantom/implied actions? Would it be considered a good practice?

I didn’t understand the reasoning here, could you explain a bit more?