Part 3 of this series shows examples of the CKB-VM, a RISC-V instruction set based VM, in action in three different ways.

In part 1 of this series, we introduced the Nervos CKB-Virtual Machine (CKB-VM), a RISC-V instruction set based VM for executing smart contracts and written in Rust. Part 2 discussed the design process and benefits of utilizing a RISC-V instruction set. Now, in part 3, let's explore some specific examples.

 

Example Contract

The following example shows a minimal contract that can run on the CKB-VM:
 

 

                    int main()
{
 return 0;
}
                  

The following command can be used to compile the code with GCC:

 

                    riscv64-unknown-elf-gcc main.c -o main
                  

A CKB contract is a binary file that complies with traditional Unix invocation methods. Input parameters can be provided through argc/argv, and the output result is presented by the return value of the main function.

A value of 0 indicates that contract invocation was successful, other values indicate contract invocation failure.

To simplify the example, we have implemented the contract above in C. However, any language that can be compiled to the RISC-V instruction set can be used for CKB contract development.

  • The RISC-V instruction set for the Rust language is being developed, with support for the RISC-V 32-bit instruction set being incorporated nightly. 64-bit instruction set support in LLVM is under development.
  • The RISC-V instruction set for the Go language is also being developed.
  • For higher level languages, we can directly compile their C implementations to RISC-V binaries and leverage “VM on top of VM” techniques to enable contracts written in these languages on CKB. For example, mruby can be compiled into RISC-V binaries to enable Ruby-based contract development. This method can be utilized with the MicroPython-based Python language and the Duktape-based JavaScript language as well.

Even contracts compiled to EVM bytecode or Bitcoin Script can be compiled to CKB-VM bytecode. These contracts may have heavier running overhead (CPU cycles) than contracts implemented using lower level languages. The development time saved here and security benefits might be more valuable than incurring cycle costs in some scenarios, though we see clear advantages in legacy contract migration to more efficient bytecode.

 

Additional Requirements Met by CKB

Outside of the simplest contracts, the CKB also provides a system library to meet the following requirements:

  • Support the libc core library
  • Support dynamic linking to reduce the space occupied by the current contract. For example, a library can be provided by loading a system cell in to the VM via dynamic link.
  • Read transaction content, the CKB VM will have Bitcoin’s SIGHASH-like functions within the VM to maximize contract flexibility.

The following figure shows the CKB contract verification model based on a preceding system library:

 

CKB contract verification model

Figure 1. CKB contract verification model

 

As shown in the preceding figure, a CKB transaction consists of inputs and outputs. While the transaction may also contain Deps (dependencies that contain data or code needed when running a contract), these only affect contract implementation and are omitted from the transaction model.

Each input to the transaction references an existing cell. A transaction can overwrite, destroy or generate a cell. Consensus rules enforce that the capacity of all output cells in the transaction cannot exceed the capacity of all input cells.

 

Criteria for Verifying Contracts

The CKB VM uses the following criteria to verify contracts:

  • Each input contains an unlock script, allowing for validation that the transaction originator can utilize the cell referenced by that input. The CKB uses the VM to run the unlock script for verification. The unlock script will usually specify the signature algorithm (for example: SIGHASH-ALL-SHA3-SECP256K) and contain the signature generated by the transaction originator. A CKB contract can use an API to read transaction content to implement SIGHASH-related computing, providing maximum flexibility.
  • Each cell may contain a validation script to check whether the cell data meets the previously specified conditions. For example, we can create a cell to save user-defined tokens. After the cell is created, we verify the number of tokens in input cells is greater than or equal to the number of tokens in the output cells to ensure that no additional tokens have been created in the transaction. To enhance security, CKB contract developers can also leverage special contracts to ensure the validation scripts of a cell cannot be modified after the cell has been created.

 

Input Cell Validation Example

Based on the above description of the CKB VM security model, we will first implement a complete SIGHASH-ALL-SHA3-SECP256K1 contract to verify the provided signature and prove the transaction originator has the ability to consume the current cell.

 

                    // For simplicity, we are keeping pubkey in the contract, however this
// solution has a potential problem: even though many contracts might share
// the very same structure, keeping pubkey here will make each contract
// quite different, preventing common contract sharing. In CKB we will
// provide ways to share common contract while still enabling each user
// to embed their own pubkey.
char* PUBKEY = "this is a pubkey";
int main(int argc, char* argv[])
{
 // We need 2 arguments for this contract
 // * The first argument is contract name, this is for compatibility issue
 // * The second argument is signature for current contract input
 if (argc < 2) {
   return -1;
 }
// This function loads current transaction into VM memory, and returns the
 // pointer to serialized transaction data. Notice ckb_mmap might preprocess
 // the transaction a bit, such as removing signatures in all tx inputs to
 // avoid chicken-egg problem when signing signature.
 int length = 0;
 char* tx = ckb_mmap(CKB_TX, &length);
 if (tx == NULL) {
   return -2;
 }
// This function dynamically links sha3 library to current VM memory space
 void *sha3_handle = ckb_dlopen("sha3");
 void (*sha3_func)(const char*, int, char*) = ckb_dlsym("sha3_256");
// Here we run sha3 on all the transaction data, simulating a SIGHASH_ALL process,
 // a different contract might choose to deserialize and only hash certain part
 // of the transaction
 char hash[32];
 sha3_func(tx, length, hash);
// Now we load secp256k1 module.
 void *secp_handle = ckb_dlopen("secp256k1");
 int (*secp_verify_func)(const char*, int, const char*, int, const char*, int) =
     ckb_dlsym("secp256k1_verify");
int result = secp_verify_func(argv[1], strlen(argv[1]),
                               PUBKEY, strlen(PUBKEY),
                               hash, 32);
if (result == 1) {
   // Verification success, we are returning 0 to indicate contract succeeds
   return 0;
 } else {
   // Verification failure
   return -3;
 }
}

                  

In this example, we first read all of the transaction content into the VM to obtain a SHA3 hash of the transaction data, and will verify that the public key specified in the contract has signed this data. We provide this SHA3 transaction data hash, the specified public key and the signature provided by the transaction originator to the secp256k1 module to verify the specified public key has signed the proposed transaction data.

If this verification is successful, the transaction originator can use the cell referenced by the current input and the contract is executed successfully. If this verification is not successful, the contract execution and transaction verification fail.
 

 

User Defined Token (UDT) Example

This example shows a cell validation script that implements an ERC20-like user defined token. In the interest of simplicity, only the transfer function of UDT is included, though a cell validation script could also implement other UDT functions.

 

                    int main(int argc, char* argv[]) {
 size_t input_cell_length;
 void* input_cell_data = ckb_mmap_cell(CKB_CELL_INPUT, 0, &input_cell_length);
size_t output_cell_length;
 void* output_cell_data = ckb_mmap_cell(CKB_CELL_OUTPUT, 0, &output_cell_length);
if (input_cell_data == NULL || output_cell_data == NULL) {
   return -1;
 }
void* udt_handle = ckb_dlopen("udt");
 data_t* (*udt_parse)(const char*, size_t) =
     ckb_dlsym(udt_handle, "udt_parse");
 int (*udt_transfer)(data_t *, const char*, const char*, int64_t) =
     ckb_dlsym(udt_handle, "udt_transfer");
data_t* input_cell = udt_parse(input_cell_data, input_cell_length);
 data_t* output_cell = udt_parse(output_cell_data, output_cell_length);
if (input_cell == NULL || output_cell == NULL) {
   return -2;
 }
ret = udt_transfer(input_cell, from, to, tokens);
 if (ret != 0) {
   return ret;
 }
int (*udt_compare)(const data_t *, const data_t *);
 if (udt_compare(input_cell, output_cell) != 0) {
   return -1;
 }
 return 0;
}

                  

In this example, we first call the system library to read the content of the input and output cells. Then, we load a UDT implementation dynamically and use the transfer method to convert the input.

After conversion, the content in the input and the output cells should completely match. Otherwise, we conclude that the transaction does not meet conditions specified in the validation script and the contract execution will fail.

Note: The preceding example is only used to present the VM functions of the CKB and does not indicate best practices for UDT implementation in the CKB.

 

Unlock Script Example (in Ruby)

Though the preceding examples have been written in C, the CKB VM is not limited to contracts written in C. For example, we can compile mruby, a Ruby implementation target embedded system to RISC-V binary and provide it as a common system library. In this way, Ruby can be used to write contracts such as the following unlock script:

 

                    if ARGV.length < 2
 raise "Not enough arguments!"
end
tx = CKB.load_tx
sha3 = Sha3.new
sha3.update(tx["version"].to_s)
tx["deps"].each do |dep|
 sha3.update(dep["hash"])
 sha3.update(dep["index"].to_s)
end
tx["inputs"].each do |input|
 sha3.update(input["hash"])
 sha3.update(input["index"].to_s)
 sha3.update(input["unlock"]["version"].to_s)
 # First argument here is signature
 input["unlock"]["arguments"].drop(1).each do |argument|
   sha3.update(argument)
 end
end
tx["outputs"].each do |output|
 sha3.update(output["capacity"].to_s)
 sha3.update(output["lock"])
end
hash = sha3.final
pubkey = ARGV[0]
signature = ARGV[1]
unless Secp256k1.verify(pubkey, signature, hash)
 raise "Signature verification error!"
End

                  

WebAssembly

In reviewing what we have detailed in this paper, some may ask the question: why does the CKB not use WebAssembly, given the interest it has attracted in the blockchain community?

WebAssembly is a great project and we hope it will be successful. The blockchain and broader software industry would benefit greatly from a sandbox environment that has wide support. Though WebAssembly has the potential to implement most of the features we have discussed, it does not deliver all of the benefits RISC-V can bring to the CKB VM. For example: WebAssembly is implemented using JIT and lacks a reasonable running overhead computing model.

RISC-V design began in 2010, the first version was released in 2011, and hardware began to emerge in 2012. However, WebAssembly emerged much later in 2015, with an MVP released in 2017. While we acknowledge WebAssembly has the potential to become a better VM, RISC-V currently has the advantage over WebAssembly.

We do not completely discard the use of WebAssembly in the CKB VM. Both WebAssembly and RISC-V are underlying VMs which shared similar design and instruction sets. We believe we can provide a binary translator from WebAssembly to RISC-V to ensure that WebAssembly-based blockchain innovation can be leveraged by the CKB.

Languages that only compile to WebAssembly (for example, Forest) can also be supported in the CKB.

 

A Community Around CKB

With the design of CKB VM, we aim to build a community around CKB that grows and adapts to new technology advancements freely and where manual intervention (such as hard forks) can be minimized. Nervos believes CKB-VM can achieve this vision.

 


Industry Articles are a form of content that allows industry partners to share useful news, messages, and technology with All About Circuits readers in a way editorial content is not well suited to. All Industry Articles are subject to strict editorial guidelines with the intention of offering readers useful news, technical expertise, or stories. The viewpoints and opinions expressed in Industry Articles are those of the partner and not necessarily those of All About Circuits or its writers.

Comments

0 Comments