[RFC] Exploring an essential data structure in CKB— the transaction

Nervos Network
Aug 25 · 16 min read

by Ian Yang

Our RFC (Request for Comments) process is intended to provide an open and community driven path for new protocols, improvements and best practices, so that all stakeholders can be confident about how the network is evolving.

Please find this RFC on our github repository, which contains proposals, standards and documentations related to Nervos Network.


This RFC has two parts. The first part covers the core transaction features, and the second part introduces some extensions. CKB is under active development, this will be updated when the transaction structure changes in future releases. At the time of writing, the corresponding CKB version is v0.19.0.

The diagram above is an overview of the transaction structure. Instead of explaining field by field, I’ll introduce various features the CKB transaction provides and how the fields play their roles in these features.

Part I: Core Features

CKB adopts the UTXO model. A transaction destroys some outputs created in previous transactions and creates some new outputs. We call the transaction output a cell in CKB. Thus the name cell and transaction output are interchangeable.

The following diagram shows the fields used in this layer.

The transaction destroys the cells in inputs and creates the cells in outputs.

The CKB chain packages transactions into blocks. We can use block number to refer to a block in the chain, which is an increasing non-negative integer starting from 0, the genesis block. The transactions in a block are also ordered. We say a block is older if it has a smaller block number, a transaction is older if either it is in an older block, or its position in a block is before another transaction.

In the following example, Block i is older than Block i +1 Transaction tx1 is older than tx2 and is older than tx3.

A live cell is the one that appears as an output but not as an input in all the older transactions. A dead cell has already been used as an input in any older transaction. A transaction can only use live cells as inputs.

We can compute the transaction hash from all transaction fields except witnesses (see Appendix A how to calculate the transaction hash).

The transaction hash is considered unique. Since a cell is always created by a transaction, and every new cell has its position in the transaction outputs array, we can refer to a cell by transaction hash and outputs index. The structure OutPoint is a reference type. The transaction uses OutPoint in inputs to reference the previously created cells instead of embedding them.

The cell stores the CKB Token in the field capacity. A transaction cannot mint capacities from the air, so a transaction must meet the following rule:

sum(cell's capacity for each cell in inputs)
≥ sum(cell's capacity for each cell in outputs)

Miners can collect the difference as a fee.

fee = sum(cell's capacity for each cell in inputs
- sum(cell's capacity for each cell in outputs)

If you are familiar with Bitcoin, you’ll find that the Value Storage layer is similar, but lacking the locking script to protect the transaction output ownership. CKB does have that feature — but before we explore that, let‘s discuss Cell Data and Code Locating layers, which are the dependencies of any scripting feature in CKB.


Instead of holding only the token value, CKB cells can store arbitrary data as well.

The field outputs_data is a parallel array of outputs. The data of the i-th cell in outputs is the i-th item in outputs_data.

The capacity in the cell is not only the amount of the stored tokens, but it is also a limit on how much data the cell can store. That's where the name comes from—it is the storage capacity of the cell.

The capacity is not only used to store data—it also has to cover all the fields in the cell, including data, lock, type, and capacity itself.

The specification to compute occupied capacity will have its RFC in the future, which is currently a draft.

The transaction must create an output cell which occupied capacity is less than the cell capacity.

occupied(cell) ≤ cell's capacity

The cell has two fields who’s type is Script. The CKB VM will run the lock scripts of all the cells in inputs, and run the type scripts of all the cells in both inputs and outputs.

We differentiate the terms script and code.

  • A script is the script structure.
  • The code is the RISC-V binary.
  • A code cell is cell who’s data is code.

The script does not include the code directly. See the script structure below. Let’s ignore the hash type Type and the field args for now.

When a CKB VM needs to run a script, it must find its code first. The fields code_hash and hash_type are used to locate the code.

In CKB, the script code is compiled into RISC-V binary. The binary is stored as the data in a cell. When hash_type is "Data", the script locates a cell in which data hash equals the script's code_hash. The cell data hash, as the name suggests, is computed from the cell data (see Appendix A). The scope is limited in the transaction; script can only find a matched cell from cell_deps.

The following diagram shows how CKB finds a matched script code.

If you want to use a script in CKB, follow the code locating rules:

  • Compile your code into RISC-V binary. You can find some examples in the repository which builds the code for system cells.
  • Create a cell which stores the binary as data in a transaction, and send the transaction to the chain.
  • Construct a script structure, in which hash_type is "Data", and code_hash is just the hash of the built binary.
  • Use the script as the type or the lock script in a cell.
  • If the script has to run in a transaction, include the code cell out point in the cell_deps.

The cells in cell_deps must be live, just like inputs. Unlike inputs, a cell only used in cell_deps is not considered dead.

The following two sections will talk about how the script is used in a transaction to lock the cells and establish contracts on cells.


Every cell has a lock script. The lock script must run when the cell is used as an input in a transaction. When the script only appears in the outputs, it is not required to reveal the corresponding code in cell_deps. A transaction is valid only when all the lock scripts in the inputs exit normally. Since the script runs on outputs, it acts as the lock to control who can unlock and destroy the cell, as well as spend the capacity stored in the cell.

Following is an example lock script code which always exits normally. Anyone can destroy the cell if it uses the code as the lock script.

int main(int argc, char *argv[]) {
return 0;
}

The most popular way to lock a digital asset is the digital signature created by asymmetric cryptography.

The signature algorithm has two requirements:

  • The cell must contain the information of the public key, so only the real private key can create a valid signature.
  • The transaction must contain the signatures, which usually signs the whole transaction as the message.

In CKB, the public key fingerprint can be stored in the args field in the script structure, and the signature can be stored in the witnesses fields in the transaction. I use "can" because it is just the recommended way, and is used in the default secp256k1 lock script. The script code is able to read any part of a transaction, so the lock script can choose a different convention, for example, storing the public key information in the cell data. However, if all the lock scripts follow the recommended convention, it can simplify the apps which create transactions, such as a wallet.

Now let’s see how the script code is located and loaded, and how the code accesses inputs, script args, and witnesses.

First, note that CKB does not run the lock script input by input. It first groups the inputs by lock script and runs the same script only once. CKB runs a script in 3 steps: script grouping, code locating and running.

The diagram above shows the first two steps.

  1. First, CKB groups inputs by lock script. In the example transaction, there are two different lock scripts used in inputs. Although they locate to the same code, they have different args. Let’s focus on g1. It has two inputs with index 0 and 2. The script and the input indices will be used in step 3 later.
  2. Then CKB locates the code from cell deps. It resolves to the cell with data hash Hs and will use its data as the code.

Now CKB can load the script code binary and run the code, starting from the entry function. The args is passed as the arguments to the entry function. Following the C convention, we prepend an empty argument at the beginning, which is expected to be the program name.

Various CKB syscalls are designed to read data from the transaction. These syscalls have an argument to specify where to read the data. For example, to load the first witness:

ckb_load_witness(addr, len, offset, 0, CKB_SOURCE_INPUT);

The first arguments control where to store the read data and how many bytes to read. Let’s ignore them in the following paragraphs.

The fifth argument is the data source. CKB_SOURCE_INPUT means reading from transaction inputs, and the fourth argument 0 is the index into the inputs array.

There's no source like CKB_SOURCE_WITNESS, since the witnesses is a parallel array of inputs. They have a one-to-one relationship by index.

Remember that we have saved the indices of the input when grouping inputs by the lock script. This info is used to create the virtual witnesses and inputs array for the group. The code can read input or witness using the index in the virtual array via a special source CKB_SOURCE_GROUP_INPUT.

All the syscalls that read data related to the input, can use CKB_SOURCE_GROUP_INPUT and the index in the virtual inputs array, such as ckb_load_cell_* syscalls family.


Type script is very similar to lock script, with two differences:

  • Type script is optional.
  • In a transaction, CKB must run the type scripts in both inputs and outputs.

Although we can only keep one type of script in the cell, we don’t want to disrupt the different responsibilities in a single script.

The lock script is only executed for outputs, so its primary responsibility is protecting the cells. Only the owner is allowed to use the cell as input, and spend the token stored along with it.

The type script is intended to establish some contracts on the cells. When you get a cell with a specified type, you can ensure that the cell has passed the verification in the specific code. And the code is also executed when the cell is destroyed. A typical scenario of type script is user-defined token. The type script must run on outputs, so the token issuance must be authorized.

Running type script on inputs is very important for contracts; for example, a contract to let a user mortgage some amount of CKB tokens to rent an asset offline. If the type script does not run on inputs, the user can get back the CKB tokens without authority from the contract by merely destroying the cells and transferring the capacity to a new cell without type script.

The steps to run type script is also similar to lock script. Except that

  1. Cells without a type script are ignored.
  2. The script group contains both inputs and outputs.

Like CKB_SOURCE_GROUP_INPUT, there's a special data source CKB_SOURCE_GROUP_OUTPUT to use the index into the virtual outputs array in the script group.

Part II: Extensions

In part I, I introduced the core features which the transaction provides. The features introduced in this part are some extensions that CKB can work without, but these extensions will improve the cell model.

The diagram below is the overview of the new fields covered in this part.

Dep Group is a cell which bundles several cells as its members. When a dep group cell is used in cell_deps, it has the same effect as adding all its members into cell_deps.

Dep Group stores the serialized list of OutPoint in cell data. Each OutPoint points to one of the group members.

The structure CellDep has a field dep_type to differentiate the normal cells which provide the code directly, and the dep groups which is expanded to its members inside cell_deps.

The dep group is expanded before locating and running code, in which only the expanded cell_deps are visible.

Example of Dep Group Expansion

In v0.19.0, the lock script secp256k1 is split into code cell and data cell. The code cell loads the data cell via cell_deps. So if a transaction needs to unlock a cell locked by secp256k1, it must add both cells in cell_deps. With dep group, the transaction only needs the dep group.

There are two reasons why we split the secp256k1 cell.

  • The code cell is small, which allows us to update it when the block size limit is low.
  • The data cell can be shared. For example, we have implemented another lock script which uses ripemd160 to verify the public key hash. This script reuses the data cell.

In Lock Script in Part I, I described how a script locates its code via cell data hash. Once a cell is created, its associated script code cannot change, since it is infeasible to find a different piece of code that has the same hash.

Script has another option for hash_type, Type.

When the script uses the hash type Type, it matches the cell in which type script hash equals the code_hash. The type script hash is computed from the cell type field (see Appendix A).

Now it is possible to upgrade code if the cell uses a script which locates code via type script hash, by creating a new cell with the same type script. The new cell has the updated code. The transaction which adds the new cell in dep_cells will use the new version.

However, this only solves one problem. It is not safe if an adversary can create a cell with the same type script but using forged code as the data. The adversary can bypass the script verification by using the fake cell as a dep. The following chapter will describe a script to solve the second problem.

Because the code referenced by type script hash can change, you must trust the script author to use this kind of type scripts. Although which version is used will depend on which cell is added in the transaction in dep_cells. The user can always inspect the code before signing the transaction. But if the script is used to unlock a cell, even the signature checking can be skipped.


There’s a reason we choose cell type script hash to support upgradable script. If the adversary wants to create a cell with a specific type script, the transaction must be verified by the type script code.

Type ID is this kind of type script. As the name suggests, it ensures the uniqueness of the type script.

This feature involves several type scripts, so I will use different terms to differentiate them:

  • The Type ID code cell is the cell which stores the code to verify that a type id is unique.
  • The Type ID code cell has a type script as well. We don’t care about the actual content for now, so let’s assume the type script hash is TI.
  • A Type ID is a type script in which hash_type is "Type", and code_hash is TI.

From Type Script Part I, we know that type script groups inputs and outputs first. In other words, if the type script is a type ID, the inputs and outputs in the group all have the same type ID.

The Type ID code verifies that, in any type id group, there is at most one input and at most one output. A transaction is allowed to have multiple type id groups depending on the numbers of inputs and outputs, the type id groups are categorized into three different types:

  • Type ID Creation Group has only one output.
  • Type ID Deletion Group has only one input.
  • Type ID Transfer Group has one input and one output.

The transaction in the diagram above has all three kinds of type id groups.

  • G1 is a Type ID Transfer Group which transfers the type id from cell1 to cell4.
  • G2 is a Type ID Deletion Group which deletes the type id along with cell2.
  • G3 is a Type ID Creation Group which creates a new type id for cell3.

In the Type ID Creation Group, the only argument in args is the hash of this transaction first CellInput structure and the output index of the cell in the group. For example, in the group g3, id3 is a hash on tx.inputs[0] and 0 (cell3's index in tx.outputs).

There are two ways to create a new cell with a specific type id.

  1. Create a transaction where the hash of tx.inputs[0] and any index equal to a specific value. Since a cell can only be used as an input once in the chain, tx.inputs[0] must be different in each transaction; this problem is equivalent to finding a hash collision, the probability of which is negligible.
  2. Destroy the old cell in the same transaction.

We assume that method 2 is the only way to create a cell equal to an existing type id. And this way requires the authorization of the original owner.

The Type ID code can be implemented only via CKB VM code, but we choose to implement it in the CKB node as a special system script, because if we want to be able to upgrade Type ID code later, it has to use itself as the type script via type script hash, which is a recursive dependency.

TI is a hash of the content which contains TI itself.

The Type ID code cell uses a special type script hash, which is just the ascii codes in hex of the text TYPE_ID.

0x00000000000000000000000000000000000000000000000000545950455f4944

Header Deps allow the script to read block headers. This feature has some limitation to ensure the transaction is determined.

We say a transaction is determined only if all the scripts in the transaction have the determined results.

Header Deps allows the scripts to read a block header where hashes are listed inheader_deps. There's another precondition that the transaction can only be added to the chain if all the blocks listed in header_deps are already in the chain (uncles excluded).

There are two ways to load a header in a script using syscall ckb_load_header:

  • Via header deps index.
  • Via an input or a cell dep. The syscall will return the block in which the cell is created if that block is listed in header_deps.

The second way to load a header has another benefit — the script knows the cell is in the loaded block. DAO withdraw transaction use it to get the block number where the capacity was deposited.

Examples of loading header

The field since prevents a transaction from being mined before a specific time. This already has its own RFC.

The field version is reserved for future usage. It must equal 0 in current version.


There are two special transactions in the system.

The first one is the cellbase, which is the first transaction in every block. The cellbase transaction has only one dummy input. In the dummy input, the previous_outpoint does not refer to any cell but set to a special value. The since must be set to the block number.

The outputs of the cellbase are the reward and transaction fees for an older block in the chain.

Cellbase is special because the output capacities do not come from inputs.

Another special transaction is the DAO withdraw transaction. It also has a portion of output capacities that do not come from inputs. This portion is the interest from locking the cells in the DAO. CKB recognizes the DAO withdraw transaction by checking whether there are any inputs using DAO as the type script.


Appendix A: Compute Various Hash

CKB uses blake2b as the default hash algorithm. We use blake256 to denote a function to get the first 256 bits of the blake2b hash with personal “ckb-default-hash”.

Transaction hash is blake256(tx_hash_digest(tx)) where tx_hash_digest is a method to serialize all fields in a transaction excluding witnesses into binary. The serialization specification is not finalized yet, by now you can get the transaction hash via the RPC _compute_transaction_hash.

Cell data hash is just blake256(data).

Script hash is blake256(serialize(script)) where serialize turns the script structure into a block of binary. The serialization specification is not finalized yet, by now you can get the script hash via the RPC _compute_script_hash.


Please review and comment on the official RFC on github . Questions? Join our Dev Group on Telegram

Nervos Network

The Nervos Network is a public blockchain ecosystem and collection of protocols aiming to solve the current challenges facing blockchains like Bitcoin and Ethereum today.

Nervos Network

Written by

Official account for the Nervos Network.

Nervos Network

The Nervos Network is a public blockchain ecosystem and collection of protocols aiming to solve the current challenges facing blockchains like Bitcoin and Ethereum today.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade