RFC: Composable Open Transaction Lock Script

RFC: Composable Open Transaction Lock Script

This document introduces a lock script on Nervos CKB that implements composable open transactions. It is inspired from the previous Open Tx Brainstorm design, with new abilities to re-order & re-arrange signed components in an open transaction.

Data Structure

Hash Array

Inspired by the original open transaction brainstorm article, we add a new hash_array data structure before the signature used by the composable open transaction lock script. The hash_array contains a list of hash item like following:

| NAME | Command | Arg1 | Arg2 |
|------|---------|------|------|
| BITS | 8       | 12   | 12   |

A hash item contains 3 items of 32 bits(4 bytes) long. The hash_array requires no length field at the beginning, a special command will mark the termination of the hash array. In a way, one can think of the hash array, as the input program of a tiny virtual machine. The purpose of this virtual machine, is to feed a blake2b hash function with data. The resulting hash from the hash function will be used as signing message for the signature.

Commands

Valid commands accepted the hash items, together with descriptions as well as accepted arguments, are introduced in this section.

First, we have some common commands:

| COMMAND | DESCRIPTION                                                 | ARG 1                 | ARG 2        |
|---------|-------------------------------------------------------------|-----------------------|--------------|
| 0x00    | Hash the full current transaction hash                      | ignored               | ignored      |
| 0x01    | Hash length of input & output cells in current script group | ignored               | ignored      |
| 0xF0    | Terminate and generate the final blake2b hash               | ignored               | ignored      |

When the virtual machine starts executing on hash_array, a blake2b hash instance is created, most commands would generate some data. Those data are put into the blake2b instance as contents to hash. For example, command 0x00 would fetch the hash of the current running transaction via a CKB syscall, then feed the transaction hash as a piece of data to the blake2b instance. Later we shall see more commands that generate data for the blake2b hash instance.

Another way of see the hash_array, is that each hash item, would generate a piece of data(except that some item do not, we can treat those hash item as generating empty data), all the data are then concatenated into a single data entry, which is then hashed via blake2b algorithm, and used as the signing message for the signature verification phase later.

Command 0x01 calculates the number of input cells and output cells in current lock script group, and feed the 2 number in 64-bit unsigned little endian format to blake2b instance to hash. This can be used to guard against open transaction aggregators from arbitrarily adding unprocessed cells.

Command 0xF0 fills a different purpose: on the one hand, it marks the end of hash_array, on the other hand, it informs the tiny virtual machine running here, that all the data have been fed to the virtual machine, and we can now generate the resulting hash from the blake2b instance. This resulting hash is also used as a signing message, for a signature verification phase later.

With the general workflow in mind, we are ready for more commands that generate data:

| COMMAND | DESCRIPTION                                   | ARG 1                 | ARG 2        |
|---------|-----------------------------------------------|-----------------------|--------------|
| 0x11    | Hash part or the whole output cell            | index of output cell  | `Cell mask`  |
| 0x12    | Hash part or the whole input cell             | index of input cell   | `Cell mask`  |
| 0x19    | Hash part or the whole output cell            | offset of output cell | `Cell mask`  |
| 0x1A    | Hash part or the whole input cell             | offset of input cell  | `Cell mask`  |

Those 4 commands would locate an input or output cell first, then generate data which is either a part of the cell, or the whole cell. The source of the cell(whether it is an input or an output cell), is denoted by the command, the index of the cell, is denoted both by the command and ARG 1:

  • For command 0x11 and 0x12, ARG 1 denotes the absolute index of the cell within current transaction.
  • For command 0x19 and 0x1A, ARG 1 denotes the offset of the specified cell. Later we shall see that 2 variables base input index and base output index are also included in the witness part, together with hash_array and signature. For command 0x19, adding ARG 1 with base input index would yield the absolute index of the specified output cell in current transaction, while for command 0x1A, adding ARG 1 with base output index would yield the absolute index of the specified input cell in current transaction. Offsets provide a way to re-order cells, so a single CKB transaction has room for multiple non-conflicting open transactions.

The data to generate from a cell, is determined by ARG 2, or the Cell mask here, valid bits in the mask include:

| BIT   | INCLUDED DATA    |
|-------|------------------|
| 0x1   | Capacity         |
| 0x2   | type.code_hash   |
| 0x4   | type.args        |
| 0x8   | type.hash_type   |
| 0x10  | lock.code_hash   |
| 0x20  | lock.args        |
| 0x40  | lock.hash_type   |
| 0x80  | Cell data        |
| 0x100 | Type script hash |
| 0x200 | Lock script hash |
| 0x400 | The whole cell   |

Here are some actual examples:

  • 0x11 0x00 0x30 0x21 would take the output cell at absolute index 3 in current transaction, then extract its capacity, followed by lock script args as data for blake2b instance to hash
  • Assuming base input index is 5, 0x1A 0x01 0x04 0x00 would take the input cell at absolute index 21 in current transaction, then use the whole cell as data for blake2b instance to hash

In addition to cells, there is also a CellInput structure associated with each input cell, providing valuable information such as since and OutPoint. Commands below provide a way to hash CellInput data:

| COMMAND | DESCRIPTION                                   | ARG 1                 | ARG 2        |
|---------|-----------------------------------------------|-----------------------|--------------|
| 0x15    | Hash part or the whole cell input structure   | index of input cell   | `Input mask` |
| 0x1D    | Hash part or the whole cell input structure   | offset of input cell  | `Input mask` |

The same procedure for locating a cell is used for locating a CellInput structure, the only difference here, lies in the actual data to generate, or the Input mask kept in ARG 2:

| BIT  | INCLUDED DATA                 |
|------|-------------------------------|
| 0x1  | previous_output.tx_hash       |
| 0x2  | previous_output.index         |
| 0x4  | since                         |
| 0x8  | previous_output               |
| 0x10 | The whole CellInput structure |

Here are some actual examples:

  • 0x15 0x00 0x00 0x04 would take the CellInput structure at absolute index 0 in current transaction, then use its since value as data for blake2b instance to hash
  • Assuming base input index is 2, 0x1D 0x00 0x10 0x0C would take the CellInput structure at absolute index 3 in current transaction, then use its since value, followed by the serialized previous_output field(which is an OutPoint structure) as data for blake2b instance to hash

With those background in mind, we can start looking at some more complicated commands:

| COMMAND | DESCRIPTION                                                                                                                  | ARG 1                 | ARG 2         |
|---------|------------------------------------------------------------------------------------------------------------------------------|-----------------------|---------------|
| 0x21    | Push cell data to stack                                                                                                      | index of output cell  | `Data format` |
| 0x22    | Push cell data to stack                                                                                                      | index of input cell   | `Data format` |
| 0x23    | Push capacity to stack                                                                                                       | index of output cell  | ignored       |
| 0x24    | Push capacity to stack                                                                                                       | index of input cell   | ignored       |
| 0x29    | Push cell data to stack                                                                                                      | offset of output cell | `Data format` |
| 0x2A    | Push cell data to stack                                                                                                      | offset of input cell  | `Data format` |
| 0x2B    | Push capacity to stack                                                                                                       | index of output cell  | ignored       |
| 0x2C    | Push capacity to stack                                                                                                       | index of input cell   | ignored       |
| 0x2F    | Concatenate ARG 1 and ARG 2, push the resulting value to stack                                                               | higher 12 bit         | lower 12 bit  |
| 0x40    | Pop the top value from stack, then convert it to data of 32 bytes to hash                                                    | ignored               | ignored       |
| 0x41    | Pop top 2 values from stack, add them, then push the result back to stack                                                    | ignored               | ignored       |
| 0x42    | Pop top 2 values from stack, subtract them, then push the result back to stack                                               | ignored               | ignored       |
| 0x43    | Pop top 2 values from stack, multiply them, then push the result back to stack                                               | ignored               | ignored       |
| 0x44    | Pop top 2 values from stack, divide them, then push the result back to stack. When divisor is zero, exit with an error code. | ignored               | ignored       |

We have been talking about a tiny virtual machine above. But all the transactions above, simply emit data for the blake2b instance. The tiny virtual machine in the composable open transaction lock script, actual maintains a stack internally. The stack can hold a maximum of 8 elements, each element is a 256-bit integer. Commands from 0x21 to 0x2F can be used to push data to the stack:

  • Command 0x2F would concatenate values stored in ARG 1 and ARG 2, then convert the resulting value to a 256-bit integer, then push it to the stack.
  • Command 0x23, 0x24, 0x2B and 0x2C would locate a cell first in methods described above, then take the capacity of the cell, convert it to a 256-bit integer, then push it to the stack.
  • Command 0x21, 0x22, 0x29 and 0x2A would locate a cell first in methods described above, then extract part of cell data following formats defined in Data format, convert it to a 256-bit integer, then push it to the stack. The exact layout for Data format is as follows:
| BITS   | MEANING                                                                                                      |
|--------|--------------------------------------------------------------------------------------------------------------|
| 0      | Endianness, 0 for little endian, 1 for big endian                                                            |
| 1 - 3  | Length of data to extract, expressed in power of 2, for example, 3 here means 8 bytes, 5 here means 32 bytes |
| 4 - 11 | Start offset of data to extract                                                                              |

Note that the stack can store a maximum of 8 elements. Pushing more data when the stack is full would result an immediate termination of the lock script with an error return code.

Commands from 0x41 to 0x44 provide basic operations on values from the top of the stack. In case of overflows/underflows, wrapping around behaviors will be used.

As a more complete example, the following program can be used to ensure that one can only extract certain amount of sUDT tokens from a particular account:

0x01 0x00 0x00 0x00    // Hash the length of input & output cells in current script group
0x1A 0x00 0x03 0x00    // Hash the lock script(account) and type script(sUDT ID) for the
                       // input cell at offset 0
0x19 0x00 0x03 0x00    // Hash the lock script(account) and type script(sUDT ID) for the
                       // output cell at offset 0
0x29 0x00 0x04 0x00    // Take the output cell at offset 0, extract the first 16 bytes of
                       // data in little endian format(sUDT amount), and push the resulting
                       // value to stack
0x2A 0x00 0x04 0x00    // Take the input cell at offset 0, extract the first 16 bytes of
                       // data in little endian format(sUDT amount), and push the resulting
                       // value to stack
0x42 0x00 0x00 0x00    // Substract the top 2 values on stack
0x40 0x00 0x00 0x00    // Hash the top value on stack
0x2B 0x00 0x00 0x00    // Take the output cell at offset 0, push the capacity to stack
0x2C 0x00 0x00 0x00    // Take the input cell at offset 0, push the capacity to stack
0x42 0x00 0x00 0x00    // Substract the top 2 values on stack
0x40 0x00 0x00 0x00    // Hash the top value on stack
0xF0 0x00 0x00 0x00    // Terminate and generate the resulting hash

An open transaction for this program would contain one input cell and one output cell. The signature provided will cover the following parts:

  • Length of input & output cells in current script group
  • Accounts used in input & output cells
  • sUDT ID used in input & output cells
  • The difference of sUDT tokens between input and output cells
  • The difference of CKBs between input and output cells

If you think about it, this program does not even enforce one to use a certain input cell. If the open transaction creator has multiple cells satisfying the requirement, the aggregator is free to pick any of the input cells, while at the same time, the aggregator can only choose to generate transaction following open transaction creator’s requirements. All tokens stay secure with no chance of being stolen.

Lock Script

A Composable Open Transaction Lock Script looks like following:

Code hash: composable open transaction script code hash
Hash type: composable open transaction script hash type
Args: <21 byte identity>

It uses the same identity data structure as RC Lock:

<1 byte flag> <20 byte identity content>

Depending on the value of the flag, the identity content has different interpretations:

  • 0x0: identity content represents the blake160 hash of a secp256k1 public key. The lock script will perform secp256k1 signature verification, just as the SECP256K1/blake160 lock, using the signing message calculated via executing the hash_array program shown above.

Later, more checks might be added to the identity data structure. For example, when exec becomes ready, we might add yet another identity type, which loads a new script for the actual identity validation.

Witness

When unlocking a composable open transaction lock, the corresponding witness must be a proper WitnessArgs data structure in molecule format, the following data structure must be present in the lock field of the WitnessArgs:

| BYTES   | CONTENT           |
|---------|-------------------|
| 0..7    | Base input index  |
| 8..15   | Base output index |
| 16..n   | Hash array        |
| n..n+65 | Signature         |

Base input index and base output index shall be filled by open transaction aggregator, while hash array and signature are provided by open transaction creator.

Examples

Unlocking an Open Transaction

CellDeps:
    <vec> Composable Open Transaction Lock Script Cell
Inputs:
    <vec> Open Transaction Cell
        Capacity: 100
        Lock:
            code_hash: Composable Open Transaction Lock
            args: <flag: 0x0> <pubkey hash 1>
    <...>
Outputs:
    <vec> Open Transaction Cell
        Capacity: 50
        Lock:
            code_hash: Composable Open Transaction Lock
            args: <flag: 0x0> <pubkey hash 1>
    <...>
Witnesses:
    WitnessArgs structure:
      Lock:
        base input index: 0
        base output index: 0
        hash array: <a valid hash array program>
      <...>

Integration

In a real development, open transaction creators can just create open transactions in the same format as a typical transaction, with base input index and base output index both filled as 0.

If we think about it, most of such open transactions can also be submitted and accepted by CKB, but an open transaction aggregator would love to combine multiple such transaction in a single one, collecting payments and save transaction fees.

9 Likes
  1. Why there is no op-code to hash witness?
  2. What’s the endianness of op code 0x40 (convert that 256-bit integer to which endianness)?

since field?

  1. I’m not sure if there’s the need for hashing witness in case of open transaction
  2. We can just pick one here.

Does the difference here mean that the amounts of sUDT/CKB token between output and input?