CKB Transaction CoBuild Protocol Overview

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