Skip to main content

Tutorial: Run JavaScript Code on CKB

⏰ Estimated Time: 5 - 7 min
🔧 What You Will Need:
For detailed installation steps, refer to our Installation Guide

Tutorial Overview

As you have learned before, it's possible to use any programming language to write a Script (Smart contract) for CKB. But how practical is it in reality? In this tutorial, you will see a full example of using JavaScript to write and execute Scripts within the CKB-VM

.

The process is as follows:

  1. Port a JavaScript Engine: You will learn how to port a JavaScript engine as a base Script to run on CKB.
  2. Write Business Logic in JavaScript: You will learn how to build and execute JavaScript business logic within this base Script on the CKB-VM.

This might sound complex, but thanks to the CKB-VM team, we already have a fully runnable JavaScript engine called ckb-js-vm, which was ported from quick.js and optimized for CKB-VM. We will deploy the engine on a CKB devnet and use it to run JavaScript-based Scripts on CKB.

Follow the step-by-step guide below, or check out the full code example on Github.

Get ckb-js-vm Binary

The ckb-js-vm is a binary that can be used both in the CLI and in the on-chain CKB-VM. Let's first build the binary and give it a try to see if it works as expected.

info

You will need Clang ≥v18 to build the ckb-js-vm binary:

git clone https://github.com/nervosnetwork/ckb-js-vm
cd ckb-js-vm
git submodule update --init
make all

Now, the binary is in the build/ folder. Without writing any codes, we can use the CKB-Debugger(another CLI tool that enables off-chain Script development, as the name suggests) to run the ckb-js-vm binary for a quick test.

Install CKB-Debugger

Install CKB-Debugger using cargo. We recommend using ≥v0.113.0.

cargo install --git https://github.com/nervosnetwork/ckb-standalone-debugger ckb-debugger

On MacOS, the protoc binary must be available to compile ckb-vm-pprof-converter. This can be installed via Homebrew:

brew install protobuf

Quick Test with CKB-Debugger

Now let's run the ckb-js-vm with some JS test codes.

Make sure you are in the root of the ckb-vm-js project folder:

ckb-debugger --read-file tests/examples/hello.js --bin build/ckb-js-vm -- -r

With the -r option, ckb-js-vm will read a local JS file via CKB-Debugger. This function is intended for testing purposes and does not function in a production environment. However, we can see the running output, which includes a hello, world message. The run result is 0, indicating that the hello.js Script executes successfully. Also, you can see how many cycles(the overhead required to execute a Script) are needed to run the JS Script in the output as well.

Integrate ckb-js-vm

ckb-js-vm offers different ways to be integrated into your own Scripts. In the next step, we will set up a project and writing codes to integrate ckb-js-vm with JavaScript code to gain a deeper understanding.

The first step is to create a new Script project. Init a folder named ckb-js-script.

mkdir ckb-js-script

Our project relies on ckb-js-vm, so we need to include it in the project. Create a new folder named deps in the root of our workspace:

cd ckb-js-script
mkdir deps

Copy ckb-js-vm/tools/compile.awk and the ckb-js-vm binary we built before into the deps folder. When you're done, it should look like this:

--deps
--compile.awk
--ckb-js-vm
...

Everything looks good now!

The simplest way to run JavaScript code using ckb-js-vm is via a Script.

Let's take a look at ckb-js-vm Script structure:

code_hash: <code_hash to ckb-js-vm cell>
hash_type: <hash_type>
args: <ckb-js-vm args, 2 bytes> <code_hash to JavaScript code cell, 32 bytes> <hash_type to
javascript code cell, 1 byte> <javascript code args, variable length>
note

2 bytes ckb-js-vm args are reserved for further use

We can pass the JavaScript code as script_args to the ckb-js-vm base Script. In this way, we can execute our own js code and integrate the ckb-js-vm in our project.

Write a simple hello.js Script

cd ckb-js-script
mkdir js/build
touch js/hello.js

Fill the hello.js with the following code:

ckb-js-script/js/hello.js
console.log("hello, ckb-js-script!");

Compile the hello.js into binary with CKB-Debugger

ckb-debugger --read-file js/hello.js --bin deps/ckb-js-vm -- -c | awk -f deps/compile.awk | xxd -r -p > js/build/hello.bc

If you are using Mac, change awk to gawk which can be installed by brew install gawk:

ckb-debugger --read-file js/hello.js --bin deps/ckb-js-vm -- -c | gawk -f deps/compile.awk | xxd -r -p > js/build/hello.bc

Test hello.js Script on Devnet

Now let's assemble all the Scripts and run them in a single CKB transaction. We will use offckb and CCC SDK, which allows us to test it locally.

First, start a local blockchain:

offckb node

Open a new terminal, deploy the ckb-js-vm and hello.bc binaries to the blockchain, resulting in 2 Scripts in Live Cells.

offckb deploy --target ./deps/ckb-js-vm --type-id
offckb deploy --target ./js/build/hello.bc --type-id

Open another terminal and start a offckb Nodejs Repl to build the transaction:

offckb repl -r
Welcome to OffCKB REPL!
[[ Default Network: devnet, enableProxyRPC: true, CCC SDK: 0.0.16-alpha.3 ]]
Type 'help()' to learn how to use.
OffCKB >

Build the transaction to test our hello Script.

First, we will construct an output Cell that carries a special Type Script to execute the hello.js codes. The code_hash and hash_type in the Type Script reference the ckb-js-vm Script Cell. The information can directly access from the REPL with myScripts variable:

OffCKB > let baseScriptArgs = `0x0000${myScripts['hello.bc'].codeHash.slice(2)}0${ccc.hashTypeToBytes(myScripts['hello.bc'].hashType).toString()}`;
OffCKB > let baseScript = ccc.Script.from({
... codeHash: myScripts["ckb-js-vm"].codeHash,
... hashType: myScripts["ckb-js-vm"].hashType,
... args: baseScriptArgs,
... })

The key here is the args of the Type Script. We locate the Cell that carries our hello.js codes and insert the reference information—which includes code_hash and hash_type–of that Cell into the args, following the args structure of ckb-js-vm.

OffCKB > let tx = ccc.Transaction.from({
... outputs: [
... {
... lock: accounts[0].lockScript,
... type: baseScript
... },
... ],
... cellDeps: [
... ...myScripts["ckb-js-vm"].cellDeps.map(c => c.cellDep),
... ...myScripts['hello.bc'].cellDeps.map(c => c.cellDep)
... ],
... });

Also, don't forget to add all the Live Cells containing the related Scripts in the cellDeps in the transaction.

Finally, we sign and send the transaction:

OffCKB > let signer = new ccc.SignerCkbPrivateKey(client, accounts[0].privkey);
OffCKB > await tx.completeInputsByCapacity(signer);
2
OffCKB > await tx.completeFeeBy(signer, 1000);
[ 0, true ]
OffCKB > await signer.sendTransaction(tx);
'0x1464fec71bd95c4caa2a2640f1573850b25ba9c696c1bfdbe0dc7459717c734b'
OffCKB >

Debug the transaction to See If It works expected

offckb debug --tx-hash 0x1464fec71bd95c4caa2a2640f1573850b25ba9c696c1bfdbe0dc7459717c734b

The logs show hello, ckb-js-script!, indicating our JavaScript code executed successfully.

Write a fib.js Script

We can try a different JavaScript example. Let's write a fib.js in the js folder:

ckb-js-script/js/fib.js
console.log("testing fib");
function fib(n) {
if (n <= 0) return 0;
else if (n == 1) return 1;
else return fib(n - 1) + fib(n - 2);
}
var value = fib(10);
console.assert(value == 55, "fib(10) = 55");

Compile the fib.js into Binary with CKB-Debugger

ckb-debugger --read-file js/fib.js --bin deps/ckb-js-vm -- -c | awk '/Run result: 0/{exit} {print}' | xxd -r -p > js/build/fib.bc

Build a New Transaction to Test fib.js Script

We repeat the same process as above:

Deploy the fib Script:

offckb deploy --target ./js/build/fib.bc --type-id

Build and send the transaction:

OffCKB > let baseScriptArgs = `0x0000${myScripts['fib.bc'].codeHash.slice(2)}0${ccc.hashTypeToBytes(myScripts['fib.bc'].hashType).toString()}`;
OffCKB > let baseScript = ccc.Script.from({
... codeHash: myScripts["ckb-js-vm"].codeHash,
... hashType: myScripts["ckb-js-vm"].hashType,
... args: baseScriptArgs,
... })
OffCKB > let tx = ccc.Transaction.from({
... outputs: [
... {
... lock: accounts[0].lockScript,
... type: baseScript
... },
... ],
... cellDeps: [
... ...myScripts["ckb-js-vm"].cellDeps.map(c => c.cellDep),
... ...myScripts['fib.bc'].cellDeps.map(c => c.cellDep)
... ],
... });
OffCKB > let signer = new ccc.SignerCkbPrivateKey(client, accounts[0].privkey);
OffCKB > await tx.completeInputsByCapacity(signer);
2
OffCKB > await tx.completeFeeBy(signer, 1000);
[ 0, true ]
OffCKB > await signer.sendTransaction(tx);
'0x3f8d8f0e4ef3be052ecb2784d058a5e88d700831783524f7295b23df047897d0'
OffCKB >

Debug the transaction to See If It works expected

offckb debug --tx-hash 0x1464fec71bd95c4caa2a2640f1573850b25ba9c696c1bfdbe0dc7459717c734b

Congratulations!

By following this tutorial so far, you have mastered how to write Scripts that integrates ckb-js-vm to execute JavaScript codes on CKB. Here's a quick recap:

  • Use offckb REPL to quickly prototype with JavaScript code and CKB bundles
  • Use Script code_hash and hash_type to build the base layer layer of ckb-js-vm.
  • Build args for the Script to carry the reference info of the JavaScript code Cell

Additional Resources