My goal in writing this tutorial is that readers will walk away with an understanding of how to go from idea to implementation on Nervos Network's layer 1 blockchain, the Common Knowledge Base (CKB). In the process, we will have the opportunity to learn about, design, and build both fungible and non-fungible tokens (NFTs).
Because this tutorial is lengthy, I have split it up into multiple parts. This first part focuses on grokking the fundamental concepts of CKB programming that you need to know before you even write a line of code.
If you are already familiar with the basics of CKB, the first article in this series may be unnecessary for you, and you should skip to part 2 of this series (not yet published).
Here is a table of contents (that is updated with links as articles are published):
Table of Contents
- CKB in Context
- CKB Programming Model
- Script Execution Environment & Script Groups
- Transaction Structure in Depth
- Script Args vs Witness Args
- Transaction Dependencies
Nervos DAO, Dapp Design, Project Setup
- In-Depth Look at Nervos DAO
- Dapp Design & Development Workflow
- Technical Components of a DAPP
- CKB Studio: An IDE and GUI for Transasctions
- Writing & Testing Scripts With CKB Studio
- Dapp Project Setup
Token Minter v0.1
- Overview of User Defined Token (UDT) Standard
- Issuing and Minting
- Token Transfer
Token Minter v0.2
- Enabling CKB-UDT Exchange
- Improving Token Security
- Overview of NFTs
- Defining NFTs in CKB
- Integrating NFTs into Token minter
- Is it secure & "Pentesting our NFT"
- Implementing our Improvements
- Testing our NFT with CKB Studio
CKB in Context
From Banks to Bitcoin
If I want to pay you $100, I can do this in one of two ways.
The first approach I can use is to hand you physical cash. Once the cash is in your possession, I can't spend it somewhere else because a physical object can only exist in one place at a time (half-expecting someone to correct me on this by citing quantum wackiness). In this sense, I cannot pay you with cash that I don't already have because physical possession of the cash is a pre-requisite to giving the cash to you. This physical act of passing the cash from my hands to your hands is a transfer of ownership (in this discussion, I am going to equate physical possession of cash with ownership of cash because, for the sake of the point I am going to make, ownership == ability to spend).
If I want to transfer USD to you digitally, the traditional approach here is to log into my online banking to initiate a transfer from my bank account to your bank account. The banking service verifies that I have the funds in my account, deducts the balance from my account, and deposits the funds into your account. One of the biggest reasons we need to use the banking service as an intermediary to facilitate financial operations and to store our funds is that we rely on banking entities to verify that people genuinely have the funds with which they are transacting. If I were storing my balance on my local computer, I could modify my balance whenever I wanted to, rendering the money value-less.
Bitcoin provides a store of digital money without the need for third parties to facilitate transactions and store our wealth on our behalf. In Bitcoin, there is no central third party that acts as the steward of my funds. In this way, holding and using Bitcoin is more like holding physical money on my person than it is like storing money with a banking service.
The Bitcoin network-enabled this type of decentralized-yet-digital value storage system for a single currency. The amounts are recorded in Unspent Transaction Outputs (UTXOs) that are composed of an amount and a script that enforces authorization to spend the amount.
It looks something like this:
The "value" field here stores the balance contained within the UTXO, while the script is a sequence of commands for Bitcoin's stack-based Script language. The script executes when someone attempts to use a UTXO to determine if the transaction meets the UTXO's spending conditions (e.g., if the signer of the transaction is the correct signer).
Transactions are collections of these UTXOs as inputs - which can only be used once- and outputs, which are new UTXOs that are then available for use. In this way, transactions are full descriptions of a proposed state change (an update to the global ledger that records everyone's balances) by the transaction signers.
Bitcoin is a single asset platform for decentralized money. After the release of Bitcoin, another question arose: what about other forms of value besides a single currency? Couldn't this same technology be used for storing and preserving multiple assets?
From Single- to Multi-Asset Platform
CKB extends the UTXO model of Bitcoin with a more generalized state model called "Cell Model." A Cell has four fields: capacity, data, lock, and type.
The cell's "capacity" field stores the amount of CKBytes contained within that cell, which indicates the number of bytes of space that the cell can occupy on-chain. CKBytes are the native token of Nervos Network's blockchain, CKB.
The contents of the data field are entirely up to the owner and can be data in any format, including executable code.
The lock script is responsible for authorizing cell consumption (i.e., using a cell as input). The lock script is similar to UTXO's scriptPubKey field, and, just like UTXOs, the lock script executes on a transaction's inputs.
The type-script is unique to CKB: it's responsible for verifying state changes according to user-defined rules or constraints. The best analogy is that type-scripts are to CKB what smart contract logic is to Ethereum. However, type-scripts are quite different from smart contracts on Ethereum; smart contracts have an internal state, can update that internal state, can respond to -and emit- events, and can send messages to other contracts. Type-scripts, on the other hand, do not have an internal state, do not mutate data, do not have any event-based behavior, and can't send messages. Although they don't have an internal state, they have read-access to all information within the transaction, including script user (the "script user" here being the cell to which the script is attached). Type-scripts execute on both inputs and outputs.
Just like in Bitcoin, transactions are complete descriptions of the proposed state change. The difference is that the inputs and outputs are cells instead of UTXOs.
This design enables sophisticated behavior while also minimizing on-chain resource consumption. It may sound like a limitation that on-chain scripts are read-only, do not contain persistent internal state, don't send messages to other scripts, and don't perform any event-based behavior, but this is by design. Because transactions are complete descriptions of state-transitions, on-chain scripts do not need to update state or manage an internal state; all of the data is already available within the transaction.
By enabling developers to define state-change rules in the cell's lock & type-scripts, as well as to define custom data within the cell's data field, deploying and storing many different types of assets on-chain becomes possible.
CKB Programming Model
Everything we do to build dapps on CKB is motivated by a single, simple goal: building correct transactions. To build correct transactions, we have to know how to build transactions in general and how to ensure their correctness. Since it all comes down to building correct transactions, we start by taking a look at the components of a dapp and how a transaction flows through these components.
At a high level, there are two pieces to a dapp that correspond to the two procedures we need to perform (build transactions and ensure they're correct). The two pieces are the generator and the validators.
State generation is performed by "generators," which are simply any off-chain system that builds & submits transactions to the blockchain. Generators can include web applications, side chains, wallets, layer 2 networks, and cross-chain systems.
On-chain verification scripts are responsible for validating state changes, so we sometimes refer to these scripts as "validators."
Every CKB node exposes an RPC API for which off-chain services can use to query chain state as well as submit transactions.
Generators are responsible for packaging cells up into a transaction, while validators (the on-chain verification scripts attached to the cells within the transaction) pick apart the transaction's cells and ensure that the cell's constraints have been satisfied. All scripts (both lock and type) that are attached to all cells must return success for the transaction to succeed.
Script Execution Model: Execution Context
Over the course of this tutorial series, I will continue to refine and elaborate on the script execution model, which is just a description of the interface between scripts and the transaction (and is, therefore, highly relevant to script developers). The first part of the script execution model we will cover is a script's execution context, which describes which information is available to a script in the first place.
Although scripts are attached to cells, they're not limited to a single cell: they can look inside the entire transaction. This ability enables scripts to enforce a lot of different rules about the transaction beyond its associated cell. E.g., it can enforce rules as simple as "any cell that uses this script must have at least 8 bytes of data in its data field" to "this cell can only be used in transactions in which the second output of the transaction has a capacity of 100 or more and in which the second output of the transaction's 12th byte of its data field == 1 when interpreted as a uint8"... I don't know why you would want to enforce that rule, but the point is that you could.
What this demonstrates is that although scripts are included within a transaction at the cell level, they execute at the transaction level. Said another way, the current transaction is the script's execution context.
Scripts and Transaction Dependencies
One question you may be asking is: if scripts are attached to cells, how do multiple cells use the same script? Copying the script code?
The answer is no; a cell's script fields do not actually store any code. Rather, a cell's script fields store references to another cell whose
data field contains the code to be executed. A
Script field in a cell is a simple table:
code_hash value in a cell's Script is a reference to another cell (a "code cell") whose data field contains executable code. The reason this reference field is named
code_hash is that the hash is of the code cell's data field; it is literally a hash of the code within the cell's data.
args field in the Script structure is a collection of arguments passed to the Script (maybe that goes without saying). Think of them as the Script's ARGV variable. Multiple cells can use the same Script via using the same
code_hash while also passing different
args to that Script.
The next question is: How is this cell located among all the other cells in the blockchain? Does a node that is verifying this transaction have to scan the entire cell-set to find the code cell for which the hash of its data field matches the
code_hash of the calling cell's Script?
Once again, the answer is no. Any cells that contain code that other cells in the transaction depend on must be included themselves be included in the transaction. They can't be included as inputs because that would destroy those cells after one transaction, which isn't very useful. They obviously can't be included in outputs, either.
That is why it's time to mention another big part of transactions that I've ignored until now: transaction dependencies.
Transactions have three main parts: inputs, outputs, and "deps." Deps are like inputs in that they reference cells that already exist on-chain. Unlike inputs, though, deps are not "consumed" by the transaction. Deps, Inputs, and Outputs are all just collections of cells.
Since a cell is not consumed when it is included in a transaction's
deps, it also doesn't need to be unlocked; anyone can use a cell as a dep.
This enables reusable code akin to libraries to be deployed on-chain.
Of course, this raises yet another question: If I use a dep cell created by someone else and they update the code stored in the dep cell's
data field, doesn't that change the hash of the data, thereby invalidating the
code_hash that my cell is using as a reference?
The answer here is... yes, it does. Which is why
Scripts actually have a third field called
If a cell's
Script references a dep cell by
hash_type: "data", the reference will become invalid if the cell owner updates the code.
However, if a script references a dep cell by
hash_type: "type", then the hash of a dep cell's
Type field (which is itself a Script structure) is used to identify the correct dependency instead of the dep cell's
Referencing a dep cell by the hash of its
Type field allows the owner of the dep cell to update the dep cell's code without invalidating other cells' references to that code.
This approach introduces a couple possible risks, though:
- Someone could create a new cell whose
datafield contains malicious code but whose
Typefield matches the
Typefield of the genuine dependency.. If we use the wrong cell as a cell dep, we may not even know it because the type hashes are the same
- If the owner of the dep cell performs a malicious update, or even destroys the dep cell entirely, we won't be able to use the cell that references this malicious or now-destroyed dependency. Only live cells can be used as dependencies.
These risks are very distinct: the first is about preserving the integrity of the reference, while the second is about preserving the integrity of the referent (the actual code) itself.
Another way to think about the difference between reference via data hash vs reference via type hash is:
A reference using
hash_type: "data" couples together the reference and the referent at its current state. A reference using
hash_type: "type" couples together the reference and the referent at its current state and future states.
The benefit of using
hash_type: "data" is that it inherently prevents both of the above-mentioned risks. First, another party besides the dep cell owner can't create malicious code that masquerades as the dependency because the reference and the code itself are inseparable. Second, the owner themselves cannot change the code without invalidating the reference, so they can't introduce new, malicious code into the dependency. The worst-case scenario is that the owners of the cells that depend on the dep cell redeploy the code into a new cell and include this new cell in their transactions' dependencies.
Luckily, there is a solution to this problem, but we will cover it later in this tutorial when we actually apply it to our token application. In the meantime, I encourage readers to use what they've learned in this first article to speculate on what the solution might be.
Conclusion & Summary
In this first article, we put CKB into context by comparing it against another UTXO-style blockchain (rather, The UTXO-style blockchain) Bitcoin that happens to support storage of a single asset. We covered the structure of transactions, cells, and scripts in CKB as well as how CKB separates state generation and verification. We also covered how specific code dependencies are attached to cells and the various ways we can manage references to those dependencies.
In the next article, we build on these fundamental concepts as we explore how to write contracts and test them with CKB Studio - an IDE with a GUI transaction builder.