This article introduces a Lock Script that implements Open Transaction on Nervos CKB. Inspired by the previous Open TX Brainstorm design, it has the new ability to reorder and rearrange signature components within Open Transaction.

Open the Tx Brainstorm:


https://talk.nervos.org/t/ope…

 

The data structure

 

The hash array

Inspired by the original Open TX brainstorming article, we added a new hash_array data structure in front of the signature used by composable OpenTX Lock Script. Hash_array contains a list of hash items, as shown below:

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

A Hash item contains three 32-bit (4-byte) objects. Hash_array does not require a length field at the beginning; a special command marks the end of the hash array. To some extent, we can think of a hash array as a small virtual machine input program. The purpose of this virtual machine is to feed data to the BLAKE2B hash function. The hash from the hash function will be used as the signature information for the signature.

The command

This section describes a valid command that accepts a hash item, along with its description and the parameters it accepts.

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 hash_array, a BLAKE2B hash instance is created, and most commands generate some data. This data is put into the BLAKE2B event as content to be hashed. For example, the command 0x00 will get the hash of the currently running transaction through the CKB SYSCALL and then feed the transaction hash as a data fragment to the BLAKE2B event. Later we’ll see more commands for generating data for the BLAKE2B hash object. Another way to see hash_array is to see the data that each hash object will generate (except for some projects that don’t, we can generate empty data from these hash objects), then connect all the data to a single data entry via the BLAKE2B hash algorithm and use it as the signature message for the subsequent signature verification phase. The command 0x01 counts the number of input and output cells in the current Lock Script group and provides two numbers in 64-bit unsigned small-encode format to the blake2B event for hashing. This can be used to prevent the Open TX aggregator from arbitrarily adding unprocessed cells. The command 0xf0 fills a different purpose: on the one hand, it marks the hash_array, on the other hand, it informs the small virtual machine to run all the data that has been passed to the virtual machine, and we can now generate the corresponding hash from the BLAKE2B event. This corresponding hash is also used as a signature message for later signature verification stages. Now that we have a general workflow in place, we can use more commands to 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` |

These four commands will first locate the input or output cell, and then generate data as part or the entire cell. The source of the cell (whether it is an input or output cell) is represented by the command, and the index of the cell is represented by the command and by ARG 1:

  • For commands 0x11 and 0x12, ARG 1 represents the absolute index of the cell in the current transaction.
  • For the commands 0x19 and 0x1A, ARG 1 represents offset in the specified cell. We’ll see later in the Witness two variables, base input index and base output index, as well as hash_array and the signature. For command 0x19, adding ARG 1 and base intput index produces the absolute index of the specified output cell in the current transaction, while for command 0x1A, Adding ARG 1 and the base output index produces the absolute index of the specified input cell in the current transaction. Offset provides a way to reorder cells, so there is room for many non-conflicting Open TXs in a CKB transaction.

Data generated from the cell, determined by ARG 2 or cell mask, the 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 practical examples:

  • 0x11 0x00 0x30 0x21 takes the output cell with an absolute index of 3 in the current transaction, then extracts its capacity and then uses the Lock Script parameter as the data hash for the BLAKE2B event
  • Assuming the base input index is 5,0 x1A 0x01 0x04 0x00 takes the input cell with an absolute index of 21 in the current transaction, and then uses the entire cell as the data to be hashes by the BLAKE2B event

In addition to the Cell, there is a CellInput structure that is associated with each input Cell and provides valuable information, such as Since and OutPoint. The following command provides a way to hash cellInput data:

  • CellInput: https://github.com/nervosnetwork/ckb/blob/85d04c329d4478df5ca40e4161152f3eab858d59/util/types/schemas/blockchain.mol#L4 1-L44
| 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 program used to locate the cell is also used to locate the structure of the cellInput, the only difference being the actual data to be generated, or the Input mask saved 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 practical examples:

  • 0x15 0x00 0x00 0x04 takes the cellInput structure with an absolute index of 0 in the current transaction and uses its since value as the hash data for the BLAKE2B event
  • Assuming the base input index is 2,0 x1D 0x00 0x10 0x0C will use the cellInput structure of absolute index 3 in the current transaction and then use its since value, The serialized previous_output field (which is an OutPoint structure) is then used as the data for the BLAKE2B event hash

With this background in mind, we can start looking at some more complex 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 discussed a mini-virtual machine above. But all of the above transactions are just sending data for the BLAKE2B event. The mini-virtual machine in the composable Open TX lock script essentially maintains a stack internally. The stack can hold up to eight elements, each of which is a 256-bit integer. Commands from 0x21 to 0x2F can be used to push data onto the stack:

  • The command 0x2F concatenates the values stored in ARG 1 and ARG 2, then converts the resulting values to 256-bit integers, which are then pushed onto the stack.
  • The commands 0x23, 0x24, 0x2B, and 0x2C first find a cell in the method described above, then take the capacity of the cell, convert it to a 256-bit integer, and then push it onto the stack.
  • The commands 0x21, 0x22, 0x29, and 0x2A will first find a cell in the method described above, then extract part of the cell’s Data according to the format defined in the Data Format, convert it to 256-bit integers, and then push it onto the stack. The exact output of the 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 up to eight elements. When the stack is full, pushing more data causes the lock script to terminate immediately, returning an error code.

The commands from 0x41 to 0x44 provide basic operations on values at the top of the stack. For overflows/underflows, the wrapping behavior is used.

As a more complete example, the following procedure can be used to ensure that only a certain number of Sudt tokens can be withdrawn 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

The Open Transaction of this program will contain one input cell and one output cell. The signature provided includes the following sections:

  • The length of the input and output cells in the current script group
  • The accounts used in the input and output cell
  • Sudt ID for the input and output cell
  • Sudt Token difference between the input and output cells
  • CKB Token difference between input and output units

If you think about it, the program doesn’t even force a cell as input. If the Open TX constructor has more than one Cell that meets the requirements, then the aggregator is free to choose any input Cell, while the aggregator can only choose to generate the transaction based on the Open TX constructor’s requirements. So all the tokens are safe from theft.

Lock Script

A composable Open Transaction Lock Script would look like this:

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

He USES the same Identity as the RC Lock (https://talk.nervos.org/t/rfc-regulation-compliance-lock/5788) data structure:

<1 byte flag> <20 byte identity content>

Depending on the value of Flag, the contents of Identity can be interpreted differently:

  • 0x0: Identity content represents the blake160 hash of the SECP256K1 public key. The lock script will perform the signature verification for SECP256K1, just like the lock for SECP256K1 / BLAKE160, using the signature message computed by executing the hash_array program shown above.

Later, we may add more checks to the IDENTITY data structure. For example, when the exec (https://github.com/nervosnetw… When ready, we might also add another Identity type, which will load a new script for the actual Identity authentication.

Witness

When unlocking a composable Open Transaction Lock, the corresponding WITNESS must be a valid subwitness-type data structure in molecular format, and the following data structures must appear in the SUBSUBWITNESS LOCK field:

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

The Base input index and Base output index are populated by the Open Transaction aggregator, whereas the hash array and signature are provided by the creator of the Open Transaction.

sample

 

Unlock 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 real-world development, the creator of an Open Transaction could create an Open Transaction in the same format as a typical Transaction, with both the base input index and the base output index filled with 0.

If we think about it, most Open Transaction can also be committed and accepted by CKB, but the Open Transaction aggregator would prefer to combine multiple such transactions into a single Transaction in order to collect payments and save on Transaction fees.